GitHub Action
Sync from GitHub
ef78361
Raw
History Blame Contribute Delete
7.62 kB
"""Attention vs Density 4-Quadrant Scatter.
X-axis: attention_score
Y-axis: citation_density
Quadrant lines at median values.
Point size: fanout_count, hover: cluster_label.
"""
import statistics
import streamlit as st
import plotly.graph_objects as go
# Quadrant colors
QUAD_COLORS = {
"high_attn_low_density": "#10B981", # Green - Opportunity
"high_attn_high_density": "#3B82F6", # Blue - Competitive
"low_attn_low_density": "#9CA3AF", # Gray - Niche
"low_attn_high_density": "#EF4444", # Red - Crowded
}
def render_distribution(clusters: list[dict], frame: str = "all"):
"""Render attention vs density quadrant scatter chart."""
scored = [
c for c in clusters
if c.get("attention_score") is not None
and c.get("citation_density") is not None
]
if not scored:
st.info("์Šค์ฝ”์–ด๊ฐ€ ๊ณ„์‚ฐ๋œ ํด๋Ÿฌ์Šคํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
return
# 4-Quadrant business interpretation (frame-specific)
if frame == "demand":
st.markdown("""
ChatGPT Demand ํ† ํ”ฝ์„ **๊ฒ€์ƒ‰ ๋นˆ๋„**(๊ฐ€๋กœ์ถ•)์™€ **์ถœ์ฒ˜ ๊ฒฝ์Ÿ**(์„ธ๋กœ์ถ•)๋กœ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค:
- **Opportunity** (์šฐํ•˜๋‹จ): ๊ฒ€์ƒ‰ ๋นˆ๋„ ๋†’์Œ + ๊ฒฝ์Ÿ ๋‚ฎ์Œ โ†’ ์ฝ˜ํ…์ธ  ์„ ์  ๊ธฐํšŒ
- **Competitive** (์šฐ์ƒ๋‹จ): ๊ฒ€์ƒ‰ ๋นˆ๋„ ๋†’์Œ + ๊ฒฝ์Ÿ ๋†’์Œ โ†’ ์ฐจ๋ณ„ํ™” ํ•„์š”
- **Niche** (์ขŒํ•˜๋‹จ): ๊ฒ€์ƒ‰ ๋นˆ๋„ ๋‚ฎ์Œ + ๊ฒฝ์Ÿ ๋‚ฎ์Œ โ†’ ํ‹ˆ์ƒˆ ์˜์—ญ
- **Crowded** (์ขŒ์ƒ๋‹จ): ๊ฒ€์ƒ‰ ๋นˆ๋„ ๋‚ฎ์Œ + ๊ฒฝ์Ÿ ๋†’์Œ โ†’ ํฌํ™” ์˜์—ญ
""")
elif frame == "supply":
st.markdown("""
Gemini Supply ํ† ํ”ฝ์„ **์ธ์šฉ ๋นˆ๋„**(๊ฐ€๋กœ์ถ•)์™€ **์ถœ์ฒ˜ ์ง‘์ค‘๋„**(์„ธ๋กœ์ถ•)๋กœ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค:
- **Opportunity** (์šฐํ•˜๋‹จ): ์ธ์šฉ ๋นˆ๋„ ๋†’์Œ + ์ถœ์ฒ˜ ๋ถ„์‚ฐ โ†’ ์ƒˆ ์ถœ์ฒ˜ ์ง„์ž… ๊ธฐํšŒ
- **Competitive** (์šฐ์ƒ๋‹จ): ์ธ์šฉ ๋นˆ๋„ ๋†’์Œ + ์ถœ์ฒ˜ ์ง‘์ค‘ โ†’ ๊ธฐ์กด ๊ถŒ์œ„์ž ์ง€๋ฐฐ
- **Niche** (์ขŒํ•˜๋‹จ): ์ธ์šฉ ๋นˆ๋„ ๋‚ฎ์Œ + ์ถœ์ฒ˜ ๋ถ„์‚ฐ โ†’ ํ‹ˆ์ƒˆ ์˜์—ญ
- **Crowded** (์ขŒ์ƒ๋‹จ): ์ธ์šฉ ๋นˆ๋„ ๋‚ฎ์Œ + ์ถœ์ฒ˜ ์ง‘์ค‘ โ†’ ํฌํ™” ์˜์—ญ
""")
else:
st.markdown("""
ํ† ํ”ฝ์„ **AI ๊ด€์‹ฌ๋„**(๊ฐ€๋กœ์ถ•)์™€ **๊ฒฝ์Ÿ ๋ฐ€๋„**(์„ธ๋กœ์ถ•)๋กœ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค:
- **Opportunity** (์šฐํ•˜๋‹จ): AI ๊ด€์‹ฌ ๋†’์Œ + ๊ฒฝ์Ÿ ๋‚ฎ์Œ โ†’ ์ฝ˜ํ…์ธ  ์„ ์  ๊ธฐํšŒ
- **Competitive** (์šฐ์ƒ๋‹จ): AI ๊ด€์‹ฌ ๋†’์Œ + ๊ฒฝ์Ÿ ๋†’์Œ โ†’ ์ฐจ๋ณ„ํ™” ํ•„์š”
- **Niche** (์ขŒํ•˜๋‹จ): AI ๊ด€์‹ฌ ๋‚ฎ์Œ + ๊ฒฝ์Ÿ ๋‚ฎ์Œ โ†’ ํ‹ˆ์ƒˆ ์˜์—ญ
- **Crowded** (์ขŒ์ƒ๋‹จ): AI ๊ด€์‹ฌ ๋‚ฎ์Œ + ๊ฒฝ์Ÿ ๋†’์Œ โ†’ ํฌํ™” ์˜์—ญ
""")
attns = [float(c["attention_score"]) for c in scored]
densities = [float(c["citation_density"]) for c in scored]
median_attn = statistics.median(attns)
median_density = statistics.median(densities)
# Classify each point into quadrant
xs, ys, sizes, colors, hover_texts = [], [], [], [], []
quadrant_counts = {"opportunity": 0, "competitive": 0, "niche": 0, "crowded": 0}
for c in scored:
attn = float(c["attention_score"])
density = float(c["citation_density"])
fanout_count = c.get("fanout_count", 10)
label = c.get("cluster_label") or f"Cluster-{c['id'][:8]}"
xs.append(attn)
ys.append(density)
sizes.append(max(5, min(35, fanout_count / 5)))
if attn >= median_attn and density < median_density:
color = QUAD_COLORS["high_attn_low_density"]
quad = "Opportunity"
quadrant_counts["opportunity"] += 1
elif attn >= median_attn and density >= median_density:
color = QUAD_COLORS["high_attn_high_density"]
quad = "Competitive"
quadrant_counts["competitive"] += 1
elif attn < median_attn and density < median_density:
color = QUAD_COLORS["low_attn_low_density"]
quad = "Niche"
quadrant_counts["niche"] += 1
else:
color = QUAD_COLORS["low_attn_high_density"]
quad = "Crowded"
quadrant_counts["crowded"] += 1
colors.append(color)
opp = float(c.get("opportunity_score", 0) or 0)
hover_texts.append(
f"<b>{label}</b><br>"
f"Attention: {attn:.4f}<br>"
f"Density: {density:.4f}<br>"
f"Opportunity: {opp:.4f}<br>"
f"Fanouts: {fanout_count}<br>"
f"Quadrant: {quad}"
)
fig = go.Figure()
# Data points
fig.add_trace(go.Scatter(
x=xs,
y=ys,
mode="markers",
marker=dict(
size=sizes,
color=colors,
opacity=0.7,
line=dict(width=0.5, color="#333"),
),
text=hover_texts,
hoverinfo="text",
showlegend=False,
))
# Quadrant lines (add padding to avoid collapse when all values are identical)
attn_span = max(attns) - min(attns) or 0.001
density_span = max(densities) - min(densities) or 0.1
x_range = [min(attns) - attn_span * 0.1, max(attns) + attn_span * 0.1]
y_range = [min(densities) - density_span * 0.1, max(densities) + density_span * 0.1]
fig.add_hline(y=median_density, line_dash="dash", line_color="#9CA3AF", opacity=0.5)
fig.add_vline(x=median_attn, line_dash="dash", line_color="#9CA3AF", opacity=0.5)
# Quadrant labels
fig.add_annotation(x=x_range[1], y=y_range[0], text="๐ŸŸข Opportunity",
showarrow=False, font=dict(size=11, color=QUAD_COLORS["high_attn_low_density"]))
fig.add_annotation(x=x_range[1], y=y_range[1], text="๐Ÿ”ต Competitive",
showarrow=False, font=dict(size=11, color=QUAD_COLORS["high_attn_high_density"]))
fig.add_annotation(x=x_range[0], y=y_range[0], text="โšช Niche",
showarrow=False, font=dict(size=11, color=QUAD_COLORS["low_attn_low_density"]))
fig.add_annotation(x=x_range[0], y=y_range[1], text="๐Ÿ”ด Crowded",
showarrow=False, font=dict(size=11, color=QUAD_COLORS["low_attn_high_density"]))
fig.update_layout(
title="Attention vs Citation Density (4-Quadrant)",
xaxis_title="Attention Score",
yaxis_title="Citation Density",
height=600,
template="plotly_white",
hoverlabel=dict(bgcolor="white", font_size=12),
)
st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False})
# Quadrant summary with action guides
st.markdown("#### Quadrant ์š”์•ฝ")
q1, q2, q3, q4 = st.columns(4)
with q1:
st.metric(
"๐ŸŸข Opportunity", f"{quadrant_counts['opportunity']}๊ฐœ",
help="AI ๊ด€์‹ฌ๋„ ๋†’์Œ + ๊ฒฝ์Ÿ ๋‚ฎ์Œ โ†’ ์ฝ˜ํ…์ธ  ์„ ์  ๊ธฐํšŒ. "
"์ด ํ† ํ”ฝ์— ๋Œ€ํ•œ ์ „๋ฌธ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ์ž‘ํ•˜๋ฉด AI ๋‹ต๋ณ€์— ์ธ์šฉ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค.",
)
with q2:
st.metric(
"๐Ÿ”ต Competitive", f"{quadrant_counts['competitive']}๊ฐœ",
help="AI ๊ด€์‹ฌ๋„ ๋†’์Œ + ๊ฒฝ์Ÿ ๋†’์Œ โ†’ ๊ฒฝ์Ÿ ์น˜์—ด. "
"์ฐจ๋ณ„ํ™”๋œ ์ฝ˜ํ…์ธ ๋‚˜ ์ „๋ฌธ์„ฑ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.",
)
with q3:
st.metric(
"โšช Niche", f"{quadrant_counts['niche']}๊ฐœ",
help="AI ๊ด€์‹ฌ๋„ ๋‚ฎ์Œ + ๊ฒฝ์Ÿ ๋‚ฎ์Œ โ†’ ํ‹ˆ์ƒˆ ์˜์—ญ. "
"์‹œ์žฅ์ด ์„ฑ์žฅํ•˜๋ฉด ์„ ์  ํšจ๊ณผ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
)
with q4:
st.metric(
"๐Ÿ”ด Crowded", f"{quadrant_counts['crowded']}๊ฐœ",
help="AI ๊ด€์‹ฌ๋„ ๋‚ฎ์Œ + ๊ฒฝ์Ÿ ๋†’์Œ โ†’ ํฌํ™” ์˜์—ญ. "
"์ƒˆ๋กœ์šด ์ง„์ถœ๋ณด๋‹ค ๊ธฐ์กด ์ฝ˜ํ…์ธ  ์œ ์ง€์— ์ง‘์ค‘ํ•˜์„ธ์š”.",
)
st.caption(f"Median Attention: {median_attn:.4f} | Median Density: {median_density:.4f}")