structural-topic-model 0.2.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.
@@ -0,0 +1,39 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.pyo
6
+ *.pyd
7
+
8
+ # Distribution / packaging
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ *.egg
13
+ MANIFEST
14
+
15
+ # Virtual environments
16
+ .venv/
17
+ venv/
18
+ env/
19
+
20
+ # Testing
21
+ .pytest_cache/
22
+ .coverage
23
+ htmlcov/
24
+ .tox/
25
+
26
+ # Type checkers
27
+ .mypy_cache/
28
+ .ruff_cache/
29
+
30
+ # IDE / editor
31
+ .claude/
32
+ .idea/
33
+ .vscode/
34
+
35
+ # Project-specific
36
+ stm*
37
+ !pystm/stm.py
38
+ application/
39
+ uv.lock
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hirata-keisuke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: structural-topic-model
3
+ Version: 0.2.0
4
+ Summary: Python implementation of the Structural Topic Model (STM), a port of the R stm package with a scikit-learn style API
5
+ Project-URL: Homepage, https://github.com/hirata-keisuke/pystm
6
+ Project-URL: Repository, https://github.com/hirata-keisuke/pystm
7
+ Project-URL: Issues, https://github.com/hirata-keisuke/pystm/issues
8
+ Author-email: hirata-keisuke <plainpeace39th@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: NLP,STM,structural topic model,text mining,topic model
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: Text Processing :: Linguistic
19
+ Requires-Python: >=3.12
20
+ Requires-Dist: numpy>=2.0
21
+ Requires-Dist: scikit-learn>=1.9.0
22
+ Requires-Dist: scipy>=1.14
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pystm — Structural Topic Model in Python
26
+
27
+ R の [stm](https://github.com/bstewart/stm) パッケージ(Roberts, Stewart & Tingley)のコア推定アルゴリズムを Python に移植したものです。API は scikit-learn の `LatentDirichletAllocation` に倣っています。
28
+
29
+ ## STM とは
30
+
31
+ STM はロジスティック正規トピックモデルで、文書のメタデータ(prevalence 共変量)が各文書のトピック比率の事前平均をシフトさせます。共変量なしの場合は Correlated Topic Model (CTM) に帰着します。推定は semi-collapsed 変分 EM で行います(R 版 `stm()` と同一のアルゴリズム)。
32
+
33
+ ## 使い方
34
+
35
+ ```python
36
+ import numpy as np
37
+ from pystm import StructuralTopicModel
38
+
39
+ # X: (n_docs, n_vocab) の単語カウント行列(dense / scipy.sparse どちらも可)
40
+ # covar: (n_docs, n_covariates) の prevalence 共変量(切片は自動付与)
41
+
42
+ model = StructuralTopicModel(n_components=10, init="spectral")
43
+ model.fit(X, prevalence=covar)
44
+
45
+ model.theta_ # 学習文書のトピック比率 (n_docs, K)
46
+ model.components_ # トピック-単語分布 (K, V)。各行の和は1
47
+ model.gamma_ # prevalence 回帰係数 (1+P, K-1)。先頭行が切片
48
+ model.sigma_ # トピック共分散行列 (K-1, K-1)
49
+
50
+ # 新規文書の推論(fitNewDocuments 相当)
51
+ theta_new = model.transform(X_new, prevalence=covar_new)
52
+
53
+ # トピックの代表語(labelTopics 相当)
54
+ model.top_words(n_words=10) # 確率順
55
+ model.top_words(n_words=10, kind="frex") # FREX(頻度と排他性のバランス)
56
+ ```
57
+
58
+ 共変量を渡さなければ CTM として推定されます:
59
+
60
+ ```python
61
+ model = StructuralTopicModel(n_components=10).fit(X)
62
+ ```
63
+
64
+ ### content 共変量(SAGE / Distributed Multinomial Regression)
65
+
66
+ 文書のカテゴリによってトピック内の語彙の使い方が変わるモデルです。各文書に1つのカテゴリラベルを渡します:
67
+
68
+ ```python
69
+ model = StructuralTopicModel(n_components=10)
70
+ model.fit(X, prevalence=covar, content=party_labels) # 例: 政党ラベル
71
+
72
+ model.aspect_components_ # カテゴリ別トピック-語彙分布 (n_levels, K, V)
73
+ model.kappa_["params"] # ベースラインからのスパースな偏差(lasso 推定)
74
+ model.content_levels_ # カテゴリ水準
75
+ # transform / score にも同じ content を渡す
76
+ model.transform(X_new, prevalence=c_new, content=labels_new)
77
+ ```
78
+
79
+ R 版の `kappa.prior="L1"`(既定)に相当する Distributed Poisson 回帰で推定します。glmnet の代わりに、設計行列のインジケータ構造を利用して語彙方向に完全ベクトル化した IRLS+座標降下の Poisson lasso を実装しています(正則化パスと情報量規準による選択も R と同じ)。
80
+
81
+ ### estimateEffect 相当: 共変量効果の推定
82
+
83
+ トピック比率を目的変数とする回帰を method of composition(変分事後分布からの θ サンプリング)で行い、測定不確実性込みの係数を返します:
84
+
85
+ ```python
86
+ from pystm import estimate_effect
87
+
88
+ eff = estimate_effect(model, covar, uncertainty="Global", nsims=25)
89
+ tables = eff.summary() # {topic: 構造化配列(estimate/std_error/t_value/p_value)}
90
+ tables[0]["estimate"] # トピック0の回帰係数(先頭が切片)
91
+ ```
92
+
93
+ `uncertainty="Global"`(推奨・既定)と `"None"` をサポートします(R の `"Local"` は未実装)。
94
+
95
+ ### searchK 相当: トピック数の選択
96
+
97
+ ```python
98
+ from pystm import search_k
99
+
100
+ res = search_k(X, K_values=[5, 10, 15], prevalence=covar,
101
+ model_params={"max_iter": 100})
102
+ res["heldout"] # document completion による heldout 対数尤度
103
+ res["residual"] # Taddy (2012) の残差分散(1 に近いほど良い)
104
+ res["semcoh"] # 意味的一貫性 / res["exclus"]: 排他性
105
+ res["bound"], res["lbound"], res["em_its"]
106
+ ```
107
+
108
+ ### その他の診断
109
+
110
+ ```python
111
+ from pystm import topic_corr, semantic_coherence, exclusivity, check_residuals
112
+
113
+ tc = topic_corr(model, cutoff=0.01) # トピック相関グラフ(simple 法)
114
+ tc.posadj # 正相関の隣接行列
115
+ semantic_coherence(model, X, M=10) # トピックごとの意味的一貫性
116
+ exclusivity(model, M=10) # トピックごとの排他性(content モデル不可)
117
+ check_residuals(model, X) # 残差分散検定 {dispersion, pvalue, df}
118
+ ```
119
+
120
+ ## R 版との対応
121
+
122
+ | R | Python |
123
+ |---|---|
124
+ | `stm(docs, vocab, K, prevalence=~x, data=meta)` | `StructuralTopicModel(n_components=K).fit(X, prevalence=design)` |
125
+ | `init.type="Spectral"` (推奨・既定) | `init="spectral"` (既定) |
126
+ | `init.type="Random"` | `init="random"` |
127
+ | `gamma.prior="Pooled"` (既定) | 実装済み(共変量ありのとき自動) |
128
+ | `sigma.prior` | `sigma_prior` |
129
+ | `emtol` / `max.em.its` | `tol` / `max_iter` |
130
+ | `model=`(フィット済みモデルから再開) | `warm_start=True`(fit を繰り返し呼ぶ。`bound_` に履歴が蓄積) |
131
+ | `content=~group`(`kappa.prior="L1"`, 既定) | `fit(X, content=labels)` |
132
+ | `interactions` | `content_interactions` |
133
+ | `fitNewDocuments()` | `transform()` |
134
+ | `labelTopics()` | `top_words()` |
135
+ | `estimateEffect()` / `summary()` | `estimate_effect()` / `.summary()` |
136
+ | `searchK()` | `search_k()` |
137
+ | `make.heldout()` / `eval.heldout()` | `make_heldout()` / `eval_heldout()` |
138
+ | `topicCorr(method="simple")` | `topic_corr()` |
139
+ | `semanticCoherence()` / `exclusivity()` / `checkResiduals()` | `semantic_coherence()` / `exclusivity()` / `check_residuals()` |
140
+ | `$theta` / `$beta` / `$sigma` / `$mu$gamma` | `theta_` / `components_` / `sigma_` / `gamma_` |
141
+ | `$beta$logbeta`(content モデル) | `aspect_components_`(確率スケール) |
142
+ | `$beta$kappa` | `kappa_` |
143
+
144
+ ### scikit-learn LDA との API 差分
145
+
146
+ - `perplexity(X)` を sklearn LDA と同様に提供(変分下界ベースの `exp(-bound/総トークン数)`。低いほど良い)。
147
+ - `warm_start=True` で sklearn 流の継続学習(R 版 `model=` 相当)。
148
+ - `components_` は正規化済みの確率分布(sklearn LDA は擬似カウント)。
149
+ - 共変量は `fit(X, prevalence=...)` / `transform(X, prevalence=...)` のキーワードで渡す。R の formula は使えないので、カテゴリ変数は事前に one-hot 等で数値化してください(`patsy` や `pandas.get_dummies` が便利)。
150
+
151
+ ### 未実装
152
+
153
+ - `gamma.prior="L1"`(prevalence 側の glmnet 依存モード)
154
+ - `kappa.prior="Jeffreys"`(content の旧推定法。R 版でも後方互換のためだけに残されている)
155
+ - `fixedintercept=FALSE`(content モデルの切片推定)
156
+ - LDA(collapsed Gibbs)初期化、`ngroups` メモ化推論、`K=0`(Lee & Mimno)
157
+ - `estimateEffect()` の `uncertainty="Local"`、formula インターフェース(スプライン `s()` 等は事前に基底展開した行列を渡せば等価)
158
+ - `topicCorr(method="huge")`(huge パッケージ依存)、`selectModel()`、`permutationTest()`、プロット関数群
159
+
160
+ また、spectral 初期化の RecoverL2 は R 版既定の quadprog の代わりにペナルティ付き NNLS による厳密に近い解法を使います(指数勾配法 `recoverEG=TRUE` 相当も `pystm._spectral.recover_l2(solver="expgrad")` として利用可能)。
161
+
162
+ ## 実装メモ(R 版からの移植で見つかった重要な点)
163
+
164
+ 1. **`update.mu` の切り替えタイミング**: R 版(`stm.control.R`)では E-step に渡す事前平均の選択を `update.mu = !is.null(mu$gamma)` で判定している。つまり**初回 E-step は共有平均(ゼロベクトル)を使い、γ が推定された 2 回目以降に文書別の事前平均 Xγ に切り替わる**。「prevalence 共変量があるか」で判定すると、初回 E-step で形状不一致または誤った事前を使うバグになる(本実装も最初これを踏んだ)。
165
+
166
+ 2. **RecoverL2 のソルバー選択**: R 版の既定は quadprog による厳密な単体制約付き QP(`recoverEG=FALSE`)。論文由来の指数勾配法(`recoverEG=TRUE`)は反復上限 500 では、**1つのトピックが支配的なコーパス(文書内でトピックが強く混ざる場合)に収束不足**となり、初期化品質が大きく劣化した(K=10 の合成データで cos 類似度 0.45 前後 vs NNLS で 0.97)。反復を 20,000 まで増やしても改善しなかったため、最適化の遅さではなく平坦な目的関数で実質停止していた。本実装はペナルティ付き NNLS(和=1 制約を重み付き行で課す)を既定とした。
167
+
168
+ 3. **Random 初期化は局所解に落ちやすい**(R 版ドキュメントの記述どおり、seed によりトピック復元が大きく変わる)。動作確認・検証には決定的な Spectral 初期化を使うこと。
169
+
170
+ 4. **gram 行列の検証方法**: スペクトル初期化の正しさは、合成データで経験 gram 行列が理論期待値 `β' E[θθ'] β`(行正規化後)と一致するかで切り分けられる(本実装では最大誤差 ~1e-3 で一致)。初期化品質が悪いときは実装バグではなく、コーパス側の共起信号の弱さ(θ の混合度)が原因のことがある。
171
+
172
+ 5. **mnreg(content 共変量)の高速化**: Distributed Poisson 回帰の設計行列は「トピック主効果 / アスペクト主効果 / 交互作用」の3グループのインジケータ列で、**各グループ内の列は互いに素な行しか触らない**。そのためグループ単位の座標降下が1回の行列演算になり、さらに全語彙が同一の設計行列を共有するので V 方向にも完全ベクトル化できる。汎用の座標降下実装と比べ同一解で大幅に高速(さらに IRLS 上限 4 / スイープ上限 8 / tol 1e-4 に絞っても β の最大差は ~2e-5)。
173
+
174
+ 6. **同梱 gadarianFit の前処理は現行 textProcessor と異なる**: パッケージ同梱の `gadarianFit`(2017年)の語彙は、現行 `textProcessor.R` の処理順(句読点除去→ストップワード除去、ダッシュ保存)では再現できない。旧版の処理順(**ストップワード除去が句読点除去より先**=アポストロフィ付きの "can't" 等がストップワードとして除去される、かつ**ダッシュ非保存**= "tax-payers"→"taxpayers")+ `lower.thresh=3` で215語が完全一致する([scripts/gadarian_prep.py](scripts/gadarian_prep.py) の `legacy_order=True`)。R 版の再現実験をする際はパッケージバージョンごとの前処理差に注意。
175
+
176
+ 7. **E-step の数値ガードが実データでは必須**: R/C++ 原実装どおりの素朴な `exp(eta)` 計算は、実コーパス(短文・偏った β・大きめ K)で BFGS の直線探索が極端な点を踏んだときに inf/NaN を発生させ、Hessian の cholesky が落ちる。η のクリップ(±200)、log と除算の下限(1e-300)、非有限解のフォールバックを `_estep.py` に追加した(通常領域の値は不変、合成データ・gadarian 検証とも退行なし)。
177
+
178
+ 8. **heldout 構築時の語彙消失**: トークンを訓練側から取り除くと、コーパス全体から消える語が生じうる。R 版 `make.heldout` は語彙を再番号付けして missing 側からも削除している。これを怠ると、その語の β が 0 になり heldout 対数尤度が -inf になる。本実装も missing 側から該当トークンを除外している。
179
+
180
+ ## R 版との検証(gadarianFit)
181
+
182
+ R パッケージ同梱の `gadarianFit`(Roberts et al. 2014 AJPS の Gadarian & Albertson 移民調査データ、K=3、prevalence = treatment*pid_rep、N=341)を参照解として、本実装を数値レベルで検証済み。再現方法:
183
+
184
+ ```bash
185
+ uv run python scripts/validate_gadarian.py # 11/11 チェック合格
186
+ ```
187
+
188
+ | 検証項目 | 結果 |
189
+ |---|---|
190
+ | コーパス再現(textProcessor + prepDocuments の移植) | 語彙215語・単語カウントともR版と**完全一致** |
191
+ | R版パラメータでの E-step bound | pystm -13575.82 vs R -13575.91(R の1反復増分 0.103 の範囲内で一致) |
192
+ | R版パラメータでの文書別 θ | 相関 > 0.9999、最大差 0.02 |
193
+ | R版の解からの EM 継続(不動点チェック) | bound 単調増加・増分は収束閾値レベル(10反復で +1.86) |
194
+ | 独立フィット(Spectral 初期化)の bound | R比 -0.18%(R は確率的 LDA 初期化なので局所解の違いは想定内) |
195
+ | トピックの対応 | 3トピックとも cos 類似度 0.88 前後、上位語ほぼ一致(worri/immigr/border、job/tax/pay、peopl/countri/come) |
196
+ | treatment 効果 | 全トピックで符号一致。有意な正の効果は +0.215 vs R +0.219 とほぼ同値 |
197
+
198
+ 注: R 版のフィット自体は LDA Gibbs 初期化(R の乱数)に依存するため完全一致は原理的に不可能。代わりに「R の解が本実装の EM の不動点になっているか」「bound 計算が R の報告値と一致するか」で実装の同一性を確認している。
199
+
200
+ ## 開発
201
+
202
+ ```bash
203
+ uv sync
204
+ uv run pytest tests/
205
+ ```
206
+
207
+ ## 他プロジェクトからの利用(配布)
208
+
209
+ 配布名・import 名ともに `pystm`。実行時依存は numpy / scipy / scikit-learn のみ
210
+ (janome / dash 等は application 用の dev 依存で、配布物には含まれない)。
211
+
212
+ ```bash
213
+ # PyPI からインストール
214
+ pip install structural-topic-model
215
+ # または
216
+ uv add structural-topic-model
217
+
218
+ # ローカルパス参照(開発中)
219
+ uv add --editable /path/to/202606_StructuralTopicModel
220
+
221
+ # Git 経由
222
+ uv add git+<リポジトリURL>
223
+
224
+ # wheel をビルド
225
+ uv build # dist/structural_topic_model-x.y.z-py3-none-any.whl
226
+ ```
227
+
228
+ 配布名は `structural-topic-model`、import 名は `pystm` のまま維持しています
229
+ (PyPI の `pystm` は別の実装に取られているため)。
230
+
231
+ ## 参考文献
232
+
233
+ - Roberts, M., Stewart, B., & Tingley, D. (2019). stm: An R Package for Structural Topic Models. *Journal of Statistical Software*, 91(2).
234
+ - Arora, S. et al. (2013). A Practical Algorithm for Topic Modeling with Provable Guarantees. *ICML*.
@@ -0,0 +1,210 @@
1
+ # pystm — Structural Topic Model in Python
2
+
3
+ R の [stm](https://github.com/bstewart/stm) パッケージ(Roberts, Stewart & Tingley)のコア推定アルゴリズムを Python に移植したものです。API は scikit-learn の `LatentDirichletAllocation` に倣っています。
4
+
5
+ ## STM とは
6
+
7
+ STM はロジスティック正規トピックモデルで、文書のメタデータ(prevalence 共変量)が各文書のトピック比率の事前平均をシフトさせます。共変量なしの場合は Correlated Topic Model (CTM) に帰着します。推定は semi-collapsed 変分 EM で行います(R 版 `stm()` と同一のアルゴリズム)。
8
+
9
+ ## 使い方
10
+
11
+ ```python
12
+ import numpy as np
13
+ from pystm import StructuralTopicModel
14
+
15
+ # X: (n_docs, n_vocab) の単語カウント行列(dense / scipy.sparse どちらも可)
16
+ # covar: (n_docs, n_covariates) の prevalence 共変量(切片は自動付与)
17
+
18
+ model = StructuralTopicModel(n_components=10, init="spectral")
19
+ model.fit(X, prevalence=covar)
20
+
21
+ model.theta_ # 学習文書のトピック比率 (n_docs, K)
22
+ model.components_ # トピック-単語分布 (K, V)。各行の和は1
23
+ model.gamma_ # prevalence 回帰係数 (1+P, K-1)。先頭行が切片
24
+ model.sigma_ # トピック共分散行列 (K-1, K-1)
25
+
26
+ # 新規文書の推論(fitNewDocuments 相当)
27
+ theta_new = model.transform(X_new, prevalence=covar_new)
28
+
29
+ # トピックの代表語(labelTopics 相当)
30
+ model.top_words(n_words=10) # 確率順
31
+ model.top_words(n_words=10, kind="frex") # FREX(頻度と排他性のバランス)
32
+ ```
33
+
34
+ 共変量を渡さなければ CTM として推定されます:
35
+
36
+ ```python
37
+ model = StructuralTopicModel(n_components=10).fit(X)
38
+ ```
39
+
40
+ ### content 共変量(SAGE / Distributed Multinomial Regression)
41
+
42
+ 文書のカテゴリによってトピック内の語彙の使い方が変わるモデルです。各文書に1つのカテゴリラベルを渡します:
43
+
44
+ ```python
45
+ model = StructuralTopicModel(n_components=10)
46
+ model.fit(X, prevalence=covar, content=party_labels) # 例: 政党ラベル
47
+
48
+ model.aspect_components_ # カテゴリ別トピック-語彙分布 (n_levels, K, V)
49
+ model.kappa_["params"] # ベースラインからのスパースな偏差(lasso 推定)
50
+ model.content_levels_ # カテゴリ水準
51
+ # transform / score にも同じ content を渡す
52
+ model.transform(X_new, prevalence=c_new, content=labels_new)
53
+ ```
54
+
55
+ R 版の `kappa.prior="L1"`(既定)に相当する Distributed Poisson 回帰で推定します。glmnet の代わりに、設計行列のインジケータ構造を利用して語彙方向に完全ベクトル化した IRLS+座標降下の Poisson lasso を実装しています(正則化パスと情報量規準による選択も R と同じ)。
56
+
57
+ ### estimateEffect 相当: 共変量効果の推定
58
+
59
+ トピック比率を目的変数とする回帰を method of composition(変分事後分布からの θ サンプリング)で行い、測定不確実性込みの係数を返します:
60
+
61
+ ```python
62
+ from pystm import estimate_effect
63
+
64
+ eff = estimate_effect(model, covar, uncertainty="Global", nsims=25)
65
+ tables = eff.summary() # {topic: 構造化配列(estimate/std_error/t_value/p_value)}
66
+ tables[0]["estimate"] # トピック0の回帰係数(先頭が切片)
67
+ ```
68
+
69
+ `uncertainty="Global"`(推奨・既定)と `"None"` をサポートします(R の `"Local"` は未実装)。
70
+
71
+ ### searchK 相当: トピック数の選択
72
+
73
+ ```python
74
+ from pystm import search_k
75
+
76
+ res = search_k(X, K_values=[5, 10, 15], prevalence=covar,
77
+ model_params={"max_iter": 100})
78
+ res["heldout"] # document completion による heldout 対数尤度
79
+ res["residual"] # Taddy (2012) の残差分散(1 に近いほど良い)
80
+ res["semcoh"] # 意味的一貫性 / res["exclus"]: 排他性
81
+ res["bound"], res["lbound"], res["em_its"]
82
+ ```
83
+
84
+ ### その他の診断
85
+
86
+ ```python
87
+ from pystm import topic_corr, semantic_coherence, exclusivity, check_residuals
88
+
89
+ tc = topic_corr(model, cutoff=0.01) # トピック相関グラフ(simple 法)
90
+ tc.posadj # 正相関の隣接行列
91
+ semantic_coherence(model, X, M=10) # トピックごとの意味的一貫性
92
+ exclusivity(model, M=10) # トピックごとの排他性(content モデル不可)
93
+ check_residuals(model, X) # 残差分散検定 {dispersion, pvalue, df}
94
+ ```
95
+
96
+ ## R 版との対応
97
+
98
+ | R | Python |
99
+ |---|---|
100
+ | `stm(docs, vocab, K, prevalence=~x, data=meta)` | `StructuralTopicModel(n_components=K).fit(X, prevalence=design)` |
101
+ | `init.type="Spectral"` (推奨・既定) | `init="spectral"` (既定) |
102
+ | `init.type="Random"` | `init="random"` |
103
+ | `gamma.prior="Pooled"` (既定) | 実装済み(共変量ありのとき自動) |
104
+ | `sigma.prior` | `sigma_prior` |
105
+ | `emtol` / `max.em.its` | `tol` / `max_iter` |
106
+ | `model=`(フィット済みモデルから再開) | `warm_start=True`(fit を繰り返し呼ぶ。`bound_` に履歴が蓄積) |
107
+ | `content=~group`(`kappa.prior="L1"`, 既定) | `fit(X, content=labels)` |
108
+ | `interactions` | `content_interactions` |
109
+ | `fitNewDocuments()` | `transform()` |
110
+ | `labelTopics()` | `top_words()` |
111
+ | `estimateEffect()` / `summary()` | `estimate_effect()` / `.summary()` |
112
+ | `searchK()` | `search_k()` |
113
+ | `make.heldout()` / `eval.heldout()` | `make_heldout()` / `eval_heldout()` |
114
+ | `topicCorr(method="simple")` | `topic_corr()` |
115
+ | `semanticCoherence()` / `exclusivity()` / `checkResiduals()` | `semantic_coherence()` / `exclusivity()` / `check_residuals()` |
116
+ | `$theta` / `$beta` / `$sigma` / `$mu$gamma` | `theta_` / `components_` / `sigma_` / `gamma_` |
117
+ | `$beta$logbeta`(content モデル) | `aspect_components_`(確率スケール) |
118
+ | `$beta$kappa` | `kappa_` |
119
+
120
+ ### scikit-learn LDA との API 差分
121
+
122
+ - `perplexity(X)` を sklearn LDA と同様に提供(変分下界ベースの `exp(-bound/総トークン数)`。低いほど良い)。
123
+ - `warm_start=True` で sklearn 流の継続学習(R 版 `model=` 相当)。
124
+ - `components_` は正規化済みの確率分布(sklearn LDA は擬似カウント)。
125
+ - 共変量は `fit(X, prevalence=...)` / `transform(X, prevalence=...)` のキーワードで渡す。R の formula は使えないので、カテゴリ変数は事前に one-hot 等で数値化してください(`patsy` や `pandas.get_dummies` が便利)。
126
+
127
+ ### 未実装
128
+
129
+ - `gamma.prior="L1"`(prevalence 側の glmnet 依存モード)
130
+ - `kappa.prior="Jeffreys"`(content の旧推定法。R 版でも後方互換のためだけに残されている)
131
+ - `fixedintercept=FALSE`(content モデルの切片推定)
132
+ - LDA(collapsed Gibbs)初期化、`ngroups` メモ化推論、`K=0`(Lee & Mimno)
133
+ - `estimateEffect()` の `uncertainty="Local"`、formula インターフェース(スプライン `s()` 等は事前に基底展開した行列を渡せば等価)
134
+ - `topicCorr(method="huge")`(huge パッケージ依存)、`selectModel()`、`permutationTest()`、プロット関数群
135
+
136
+ また、spectral 初期化の RecoverL2 は R 版既定の quadprog の代わりにペナルティ付き NNLS による厳密に近い解法を使います(指数勾配法 `recoverEG=TRUE` 相当も `pystm._spectral.recover_l2(solver="expgrad")` として利用可能)。
137
+
138
+ ## 実装メモ(R 版からの移植で見つかった重要な点)
139
+
140
+ 1. **`update.mu` の切り替えタイミング**: R 版(`stm.control.R`)では E-step に渡す事前平均の選択を `update.mu = !is.null(mu$gamma)` で判定している。つまり**初回 E-step は共有平均(ゼロベクトル)を使い、γ が推定された 2 回目以降に文書別の事前平均 Xγ に切り替わる**。「prevalence 共変量があるか」で判定すると、初回 E-step で形状不一致または誤った事前を使うバグになる(本実装も最初これを踏んだ)。
141
+
142
+ 2. **RecoverL2 のソルバー選択**: R 版の既定は quadprog による厳密な単体制約付き QP(`recoverEG=FALSE`)。論文由来の指数勾配法(`recoverEG=TRUE`)は反復上限 500 では、**1つのトピックが支配的なコーパス(文書内でトピックが強く混ざる場合)に収束不足**となり、初期化品質が大きく劣化した(K=10 の合成データで cos 類似度 0.45 前後 vs NNLS で 0.97)。反復を 20,000 まで増やしても改善しなかったため、最適化の遅さではなく平坦な目的関数で実質停止していた。本実装はペナルティ付き NNLS(和=1 制約を重み付き行で課す)を既定とした。
143
+
144
+ 3. **Random 初期化は局所解に落ちやすい**(R 版ドキュメントの記述どおり、seed によりトピック復元が大きく変わる)。動作確認・検証には決定的な Spectral 初期化を使うこと。
145
+
146
+ 4. **gram 行列の検証方法**: スペクトル初期化の正しさは、合成データで経験 gram 行列が理論期待値 `β' E[θθ'] β`(行正規化後)と一致するかで切り分けられる(本実装では最大誤差 ~1e-3 で一致)。初期化品質が悪いときは実装バグではなく、コーパス側の共起信号の弱さ(θ の混合度)が原因のことがある。
147
+
148
+ 5. **mnreg(content 共変量)の高速化**: Distributed Poisson 回帰の設計行列は「トピック主効果 / アスペクト主効果 / 交互作用」の3グループのインジケータ列で、**各グループ内の列は互いに素な行しか触らない**。そのためグループ単位の座標降下が1回の行列演算になり、さらに全語彙が同一の設計行列を共有するので V 方向にも完全ベクトル化できる。汎用の座標降下実装と比べ同一解で大幅に高速(さらに IRLS 上限 4 / スイープ上限 8 / tol 1e-4 に絞っても β の最大差は ~2e-5)。
149
+
150
+ 6. **同梱 gadarianFit の前処理は現行 textProcessor と異なる**: パッケージ同梱の `gadarianFit`(2017年)の語彙は、現行 `textProcessor.R` の処理順(句読点除去→ストップワード除去、ダッシュ保存)では再現できない。旧版の処理順(**ストップワード除去が句読点除去より先**=アポストロフィ付きの "can't" 等がストップワードとして除去される、かつ**ダッシュ非保存**= "tax-payers"→"taxpayers")+ `lower.thresh=3` で215語が完全一致する([scripts/gadarian_prep.py](scripts/gadarian_prep.py) の `legacy_order=True`)。R 版の再現実験をする際はパッケージバージョンごとの前処理差に注意。
151
+
152
+ 7. **E-step の数値ガードが実データでは必須**: R/C++ 原実装どおりの素朴な `exp(eta)` 計算は、実コーパス(短文・偏った β・大きめ K)で BFGS の直線探索が極端な点を踏んだときに inf/NaN を発生させ、Hessian の cholesky が落ちる。η のクリップ(±200)、log と除算の下限(1e-300)、非有限解のフォールバックを `_estep.py` に追加した(通常領域の値は不変、合成データ・gadarian 検証とも退行なし)。
153
+
154
+ 8. **heldout 構築時の語彙消失**: トークンを訓練側から取り除くと、コーパス全体から消える語が生じうる。R 版 `make.heldout` は語彙を再番号付けして missing 側からも削除している。これを怠ると、その語の β が 0 になり heldout 対数尤度が -inf になる。本実装も missing 側から該当トークンを除外している。
155
+
156
+ ## R 版との検証(gadarianFit)
157
+
158
+ R パッケージ同梱の `gadarianFit`(Roberts et al. 2014 AJPS の Gadarian & Albertson 移民調査データ、K=3、prevalence = treatment*pid_rep、N=341)を参照解として、本実装を数値レベルで検証済み。再現方法:
159
+
160
+ ```bash
161
+ uv run python scripts/validate_gadarian.py # 11/11 チェック合格
162
+ ```
163
+
164
+ | 検証項目 | 結果 |
165
+ |---|---|
166
+ | コーパス再現(textProcessor + prepDocuments の移植) | 語彙215語・単語カウントともR版と**完全一致** |
167
+ | R版パラメータでの E-step bound | pystm -13575.82 vs R -13575.91(R の1反復増分 0.103 の範囲内で一致) |
168
+ | R版パラメータでの文書別 θ | 相関 > 0.9999、最大差 0.02 |
169
+ | R版の解からの EM 継続(不動点チェック) | bound 単調増加・増分は収束閾値レベル(10反復で +1.86) |
170
+ | 独立フィット(Spectral 初期化)の bound | R比 -0.18%(R は確率的 LDA 初期化なので局所解の違いは想定内) |
171
+ | トピックの対応 | 3トピックとも cos 類似度 0.88 前後、上位語ほぼ一致(worri/immigr/border、job/tax/pay、peopl/countri/come) |
172
+ | treatment 効果 | 全トピックで符号一致。有意な正の効果は +0.215 vs R +0.219 とほぼ同値 |
173
+
174
+ 注: R 版のフィット自体は LDA Gibbs 初期化(R の乱数)に依存するため完全一致は原理的に不可能。代わりに「R の解が本実装の EM の不動点になっているか」「bound 計算が R の報告値と一致するか」で実装の同一性を確認している。
175
+
176
+ ## 開発
177
+
178
+ ```bash
179
+ uv sync
180
+ uv run pytest tests/
181
+ ```
182
+
183
+ ## 他プロジェクトからの利用(配布)
184
+
185
+ 配布名・import 名ともに `pystm`。実行時依存は numpy / scipy / scikit-learn のみ
186
+ (janome / dash 等は application 用の dev 依存で、配布物には含まれない)。
187
+
188
+ ```bash
189
+ # PyPI からインストール
190
+ pip install structural-topic-model
191
+ # または
192
+ uv add structural-topic-model
193
+
194
+ # ローカルパス参照(開発中)
195
+ uv add --editable /path/to/202606_StructuralTopicModel
196
+
197
+ # Git 経由
198
+ uv add git+<リポジトリURL>
199
+
200
+ # wheel をビルド
201
+ uv build # dist/structural_topic_model-x.y.z-py3-none-any.whl
202
+ ```
203
+
204
+ 配布名は `structural-topic-model`、import 名は `pystm` のまま維持しています
205
+ (PyPI の `pystm` は別の実装に取られているため)。
206
+
207
+ ## 参考文献
208
+
209
+ - Roberts, M., Stewart, B., & Tingley, D. (2019). stm: An R Package for Structural Topic Models. *Journal of Statistical Software*, 91(2).
210
+ - Arora, S. et al. (2013). A Practical Algorithm for Topic Modeling with Provable Guarantees. *ICML*.
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "structural-topic-model"
3
+ version = "0.2.0"
4
+ description = "Python implementation of the Structural Topic Model (STM), a port of the R stm package with a scikit-learn style API"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [
10
+ { name = "hirata-keisuke", email = "plainpeace39th@gmail.com" },
11
+ ]
12
+ keywords = ["topic model", "structural topic model", "STM", "NLP", "text mining"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Science/Research",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
+ "Topic :: Text Processing :: Linguistic",
21
+ ]
22
+ dependencies = [
23
+ "numpy>=2.0",
24
+ "scikit-learn>=1.9.0",
25
+ "scipy>=1.14",
26
+ ]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/hirata-keisuke/pystm"
30
+ Repository = "https://github.com/hirata-keisuke/pystm"
31
+ Issues = "https://github.com/hirata-keisuke/pystm/issues"
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "dash>=4.2.0",
36
+ "janome>=0.5.0",
37
+ "pandas>=3.0.3",
38
+ "plotly>=6.8.0",
39
+ "pyreadr>=0.5.6",
40
+ "pytest>=8.0",
41
+ "rdata>=1.1.0",
42
+ "snowballstemmer>=3.1.1",
43
+ ]
44
+
45
+ [build-system]
46
+ requires = ["hatchling"]
47
+ build-backend = "hatchling.build"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["pystm"]
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ only-include = ["pystm", "tests", "scripts", "README.md", "LICENSE"]
@@ -0,0 +1,31 @@
1
+ """pystm: Python implementation of the Structural Topic Model.
2
+
3
+ A port of the R ``stm`` package (Roberts, Stewart & Tingley) with an API
4
+ modeled on scikit-learn's ``LatentDirichletAllocation``.
5
+ """
6
+
7
+ from .diagnostics import (
8
+ TopicCorrelations,
9
+ check_residuals,
10
+ exclusivity,
11
+ semantic_coherence,
12
+ topic_corr,
13
+ )
14
+ from .effects import EstimatedEffects, estimate_effect
15
+ from .model_selection import eval_heldout, make_heldout, search_k
16
+ from .stm import StructuralTopicModel
17
+
18
+ __all__ = [
19
+ "StructuralTopicModel",
20
+ "estimate_effect",
21
+ "EstimatedEffects",
22
+ "search_k",
23
+ "make_heldout",
24
+ "eval_heldout",
25
+ "topic_corr",
26
+ "TopicCorrelations",
27
+ "semantic_coherence",
28
+ "exclusivity",
29
+ "check_residuals",
30
+ ]
31
+ __version__ = "0.2.0"