netzhang's picture
Deploy emb-explorer precalc demo (merged features + demo chrome) to personal namespace
9959e4f verified
Raw
History Blame Contribute Delete
9.06 kB
"""
Shared clustering summary components.
"""
import streamlit as st
import os
import pandas as pd
from shared.utils.taxonomy_tree import build_taxonomic_tree, format_tree_string, get_tree_statistics
from shared.components.representatives import render_representative_images
from shared.utils.logging_config import get_logger
logger = get_logger(__name__)
def render_taxonomic_tree_summary():
"""Render taxonomic tree summary for precalculated embeddings."""
df_plot = st.session_state.get("data", None)
labels = st.session_state.get("labels", None)
filtered_df = st.session_state.get("filtered_df_for_clustering", None)
if df_plot is not None and filtered_df is not None:
st.markdown("### Taxonomic Distribution")
# Detect available KMeans columns
kmeans_cols = sorted(
[c for c in df_plot.columns if c.startswith("KMeans (k=")],
key=lambda c: int(c.split("=")[1].rstrip(")"))
)
# Fallback for embed_explore app (has 'cluster' column directly)
has_embed_explore_cluster = 'cluster' in df_plot.columns and not kmeans_cols
# Add controls at the top of the taxonomy section
col1, col2, col3, col4 = st.columns([1.5, 1.5, 1, 1])
with col1:
if kmeans_cols:
# Precalculated app: let user pick which KMeans run
group_by = st.selectbox(
"Group by",
options=["(none)"] + kmeans_cols,
index=0,
key="taxonomy_group_by",
help="Select a KMeans result to filter taxonomy by cluster"
)
if group_by == "(none)":
group_by = None
elif has_embed_explore_cluster:
group_by = "cluster"
else:
group_by = None
with col2:
if group_by and group_by in df_plot.columns:
unique_clusters = sorted(df_plot[group_by].unique(), key=lambda x: int(x))
cluster_options = ["All"] + [str(c) for c in unique_clusters]
selected_cluster = st.selectbox(
"Cluster",
options=cluster_options,
index=0,
key="taxonomy_cluster_selector",
help="Select a specific cluster or 'All'"
)
else:
selected_cluster = "All"
with col2:
min_count = st.number_input(
"Minimum count",
min_value=1,
max_value=1000,
value=5,
step=1,
key="taxonomy_min_count",
help="Minimum number of records for a taxon to appear in the tree"
)
with col3:
tree_depth = st.slider(
"Tree depth",
min_value=1,
max_value=7,
value=7,
key="taxonomy_tree_depth",
help="Maximum depth of the taxonomy tree to display"
)
# Create a stable cache key based on the data characteristics and filter parameters
data_length = len(filtered_df)
# Use a stable string representation instead of hash for consistency
sample_uuids = filtered_df['uuid'].iloc[:min(10, len(filtered_df))].tolist()
data_id = f"{data_length}_{len(sample_uuids)}_{sample_uuids[0] if sample_uuids else 'empty'}"
cache_key = f"taxonomy_{data_id}_{group_by}_{selected_cluster}_{min_count}_{tree_depth}"
# Check if we have cached results and they're still valid
current_cache_key = st.session_state.get("taxonomy_cache_key")
cache_exists = cache_key in st.session_state
if (not cache_exists or current_cache_key != cache_key):
with st.spinner("Building taxonomy tree..."):
# Filter data based on group_by + selected_cluster
if group_by and selected_cluster != "All" and group_by in df_plot.columns:
cluster_mask = df_plot[group_by] == selected_cluster
cluster_uuids = df_plot[cluster_mask]['uuid'].tolist()
tree_df = filtered_df[filtered_df['uuid'].isin(cluster_uuids)]
display_title = f"Taxonomic Tree for {group_by} = {selected_cluster}"
else:
tree_df = filtered_df
display_title = "Taxonomic Tree for All Data"
# Build taxonomic tree for the selected data (only when needed)
tree = build_taxonomic_tree(tree_df)
stats = get_tree_statistics(tree)
tree_string = format_tree_string(tree, max_depth=tree_depth, min_count=min_count)
# Cache the results
st.session_state[cache_key] = {
'tree': tree,
'stats': stats,
'tree_string': tree_string,
'display_title': display_title
}
st.session_state["taxonomy_cache_key"] = cache_key
# Use cached results (no regeneration)
cached_data = st.session_state[cache_key]
# Show statistics
st.markdown(f"**{cached_data['display_title']}**")
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Records", f"{cached_data['stats']['total_records']:,}")
with col2:
st.metric("Kingdoms", cached_data['stats']['kingdoms'])
with col3:
st.metric("Families", cached_data['stats']['families'])
with col4:
st.metric("Species", cached_data['stats']['species'])
# Display the tree
if cached_data['tree_string']:
st.code(cached_data['tree_string'], language="text")
else:
st.info("No taxonomic data meets the display criteria. Try lowering the minimum count.")
def render_clustering_summary(show_taxonomy=False):
"""Render the clustering summary panel using cached results per KMeans run.
For the embed_explore app, when multiple KMeans runs exist on df_plot,
the user can pick which run's summary + representative images to display.
Summaries are cached per kmeans_col by `_run_kmeans` so switching is instant.
"""
df_plot = st.session_state.get("data", None)
if df_plot is None:
st.info("Summary will appear here after projection.")
return
has_images = 'image_path' in df_plot.columns
if has_images:
# embed_explore app: full clustering summary with representative images
kmeans_cols = sorted(
[c for c in df_plot.columns if c.startswith("KMeans (k=")],
key=lambda c: int(c.split("=")[1].rstrip(")")),
)
if not kmeans_cols:
st.subheader("Clustering Summary")
st.info("Run KMeans to see the clustering summary and representative images.")
return
summaries = st.session_state.get("clustering_summaries", {}) or {}
reps_by_col = st.session_state.get("clustering_representatives_by_col", {}) or {}
st.subheader("Clustering Summary")
default_idx = len(kmeans_cols) - 1 # most recent run
selected_kmeans_col = st.selectbox(
"KMeans result",
options=kmeans_cols,
index=default_idx,
key="summary_kmeans_selector",
help="Select which KMeans run to view summary + representative images for.",
)
summary_df = summaries.get(selected_kmeans_col)
representatives = reps_by_col.get(selected_kmeans_col)
if summary_df is None or representatives is None:
st.info(
f"No cached summary for {selected_kmeans_col}. "
"Re-run KMeans with this k to regenerate it."
)
return
logger.debug(f"Displaying cached clustering summary for {selected_kmeans_col}")
st.dataframe(summary_df, hide_index=True, width='stretch')
st.markdown("#### Representative Images")
def _resolve_local_image(idx):
"""Return the local image path if it exists, else None (fallback)."""
path = df_plot.iloc[idx]["image_path"]
if isinstance(path, str) and os.path.exists(path):
return path
return None
def _local_caption(idx):
path = df_plot.iloc[idx]["image_path"]
return os.path.basename(path) if isinstance(path, str) else None
render_representative_images(
representatives,
resolve_image=_resolve_local_image,
n_per_cluster=3,
caption_fn=_local_caption,
)
else:
# Precalculated app: show taxonomy tree (works with or without KMeans)
if show_taxonomy:
filtered_df = st.session_state.get("filtered_df_for_clustering", None)
if filtered_df is not None:
render_taxonomic_tree_summary()