NoeFlandre/osm_stats / scripts /profile_clusters_standardize_first.py
NoeFlandre's picture
download
raw
3.26 kB
"""End-to-end TF-IDF pipeline on the **standardize-first** cache.
Shape-equivalent to ``scripts/profile_clusters.py`` (the filter-first
TF-IDF pipeline), but reads the standardize-first cache and writes
to ``output/standardize_first/tfidf/``. The two TF-IDF outputs are
first-class and never overwrite each other.
Run with:
.venv/bin/python -m scripts.profile_clusters_standardize_first
"""
import time
from pathlib import Path
from src.core.features.cluster import cluster_tags
from src.core.features.cluster_memberships import save_cluster_memberships
from src.core.features.medoids import compute_cluster_medoids
from src.core.features.profile import profile_clusters_by_base_key
from src.core.features.reduce import reduce_dimensions
from src.core.features.render import render_profile_markdown
from src.core.features.tfidf import build_char_tfidf_matrix
from src.core.storage.cache import read_cache_df
CACHE = "/Volumes/Seagate M3/tag_features_standardize_first.sqlite"
OUTPUT = Path("output/standardize_first/tfidf/cluster_profile.md")
MEMBERSHIPS_OUTPUT = Path("output/standardize_first/tfidf/cluster_memberships.csv")
MIN_COUNT = 500
def main() -> None:
t0 = time.time()
df = read_cache_df(CACHE, min_count=MIN_COUNT)
print(f"load: {time.time()-t0:.1f}s rows: {len(df):,}")
t0 = time.time()
matrix, _ = build_char_tfidf_matrix(df["feature"].tolist())
print(f"tfidf: {time.time()-t0:.1f}s shape: {matrix.shape}")
t0 = time.time()
dense = reduce_dimensions(matrix, n_components=50)
print(f"svd: {time.time()-t0:.1f}s shape: {dense.shape}")
t0 = time.time()
labels = cluster_tags(dense, min_cluster_size=5, min_samples=2)
n_clusters = len(set(labels) - {-1})
n_noise = int((labels == -1).sum())
print(
f"hdbscan: {time.time()-t0:.1f}s clusters: {n_clusters:,} noise: {n_noise:,}"
)
t0 = time.time()
medoids = compute_cluster_medoids(
features=df["feature"].tolist(),
dense=dense,
labels=labels,
counts=df["count_all"].tolist(),
)
print(f"medoids: {time.time()-t0:.1f}s rows: {len(medoids):,}")
t0 = time.time()
profile = profile_clusters_by_base_key(medoids, top_n=5)
print(f"profile: {time.time()-t0:.1f}s base_keys: {len(profile)}")
md = render_profile_markdown(profile)
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(md + "\n")
print(f"wrote: {OUTPUT}")
# Persist the per-cluster medoid DataFrame so downstream steps
# (env/agri breakdown, blog post) can read real per-cluster counts
# without re-running HDBSCAN.
import pandas as pd
medoids_path = OUTPUT.parent / "cluster_medoids.csv"
medoids.to_csv(medoids_path, index=False)
print(f"wrote: {medoids_path} rows: {len(medoids):,}")
# Per-tag membership CSV: one row per cache tag with its cluster
# assignment. This is the raw, unfiltered output of HDBSCAN: no
# LLM, no env/agri filter.
t0 = time.time()
memberships_path = save_cluster_memberships(labels, df, MEMBERSHIPS_OUTPUT)
print(f"memberships: {time.time()-t0:.1f}s wrote: {memberships_path}")
print()
print(profile.head(20).to_string(index=False))
if __name__ == "__main__":
main()

Xet Storage Details

Size:
3.26 kB
·
Xet hash:
2addd3a1258f47391aace52ad92df751330ed9771d315cf3cf5337aa45ee01ed

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.