chainshift-dashboard / features /research /opportunities.py
GitHub Action
Sync from GitHub
ef78361
Raw
History Blame Contribute Delete
7.54 kB
"""๊ธฐํšŒ ์˜์—ญ ๋žญํ‚น ํ…Œ์ด๋ธ” + ์ƒ์„ธ Expander.
Clusters sorted by opportunity_score DESC.
Top-10 with expanders showing sample fanouts and top sources.
v9.0: ๋น„์ฆˆ๋‹ˆ์Šค ํ•ด์„, percentile, ์ธ์šฉ ์œ ๋ฌด ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€.
"""
import statistics
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
def _get_percentile_rank(value: float, all_values: list[float]) -> int:
"""Return the percentile rank (0-100) of a value within a list."""
if not all_values:
return 0
count_below = sum(1 for v in all_values if v < value)
return int(count_below / len(all_values) * 100)
def _interpret_density(density: float, median_density: float) -> str:
"""Interpret density relative to median."""
if density < median_density * 0.5:
return "๊ฒฝ์Ÿ ๋‚ฎ์Œ"
elif density < median_density * 1.5:
return "๊ฒฝ์Ÿ ๋ณดํ†ต"
return "๊ฒฝ์Ÿ ๋†’์Œ"
def _interpret_opportunity(opp: float, median_opp: float) -> str:
"""Interpret opportunity score."""
if opp >= median_opp * 1.5:
return "๊ธฐํšŒ ํผ"
elif opp >= median_opp * 0.5:
return "๊ธฐํšŒ ๋ณดํ†ต"
return "๊ธฐํšŒ ๋‚ฎ์Œ"
def render_opportunities(clusters: list[dict], frame: str = "all"):
"""Render opportunity ranking table with detail expanders."""
scored = [
c for c in clusters
if c.get("opportunity_score") is not None
and c.get("attention_score") is not None
]
if not scored:
st.info("์Šค์ฝ”์–ด๊ฐ€ ๊ณ„์‚ฐ๋œ ํด๋Ÿฌ์Šคํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
return
# Sort by opportunity_score DESC
scored.sort(key=lambda c: float(c.get("opportunity_score", 0) or 0), reverse=True)
# Pre-compute statistics for percentile/interpretation
all_attns = [float(c.get("attention_score", 0) or 0) for c in scored]
all_densities = [float(c.get("citation_density", 0) or 0) for c in scored]
all_opps = [float(c.get("opportunity_score", 0) or 0) for c in scored]
median_density = statistics.median(all_densities) if all_densities else 0
median_opp = statistics.median(all_opps) if all_opps else 0
# Top metrics
col1, col2, col3 = st.columns(3)
with col1:
st.metric("์Šค์ฝ”์–ด๋ง ์™„๋ฃŒ", f"{len(scored)}๊ฐœ ํด๋Ÿฌ์Šคํ„ฐ")
with col2:
avg_opp = sum(all_opps) / len(all_opps)
st.metric("ํ‰๊ท  Opportunity", f"{avg_opp:.4f}")
with col3:
top = scored[0]
st.metric("Top ๊ธฐํšŒ ํ† ํ”ฝ", (top.get("cluster_label") or "N/A")[:25])
st.markdown("---")
# Explanation before ranking table (frame-specific)
if frame == "demand":
st.info(
"ChatGPT๊ฐ€ ์ž์ฃผ ๊ฒ€์ƒ‰ํ•˜์ง€๋งŒ ๊ฒฝ์Ÿ์ด ๋‚ฎ์€ Demand ํ† ํ”ฝ์ž…๋‹ˆ๋‹ค.\n\n"
"๊ธฐํšŒ ์ ์ˆ˜๊ฐ€ ๋†’์„์ˆ˜๋ก, ์ด ์ฃผ์ œ์— ์ฝ˜ํ…์ธ ๋ฅผ ๋งŒ๋“ค๋ฉด ChatGPT ๊ฒ€์ƒ‰์— ๋…ธ์ถœ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค."
)
elif frame == "supply":
st.info(
"Gemini๊ฐ€ ์ž์ฃผ ์ธ์šฉํ•˜์ง€๋งŒ ์ถœ์ฒ˜ ๊ฒฝ์Ÿ์ด ๋‚ฎ์€ Supply ํ† ํ”ฝ์ž…๋‹ˆ๋‹ค.\n\n"
"๊ธฐํšŒ ์ ์ˆ˜๊ฐ€ ๋†’์„์ˆ˜๋ก, ์ด ์ฃผ์ œ์— ์ฝ˜ํ…์ธ ๋ฅผ ๋งŒ๋“ค๋ฉด Gemini ๋‹ต๋ณ€์— ์ธ์šฉ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค."
)
else:
st.info(
"๊ธฐํšŒ ์ ์ˆ˜ Top 10 ํ† ํ”ฝ์„ ์ƒ์„ธ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. "
"๊ธฐํšŒ ์ ์ˆ˜๋Š” **'AI ๊ด€์‹ฌ๋„๊ฐ€ ๋†’์ง€๋งŒ ๊ฒฝ์Ÿ(์ธ์šฉ ์ถœ์ฒ˜)์ด ์ ์€ ํ† ํ”ฝ'**์„ ์ฐพ์•„์ฃผ๋Š” ์ง€ํ‘œ์ž…๋‹ˆ๋‹ค.\n\n"
"์ ์ˆ˜๊ฐ€ ๋†’์„์ˆ˜๋ก ์ฝ˜ํ…์ธ ๋ฅผ ๋งŒ๋“ค๋ฉด AI์— ์ธ์šฉ๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค."
)
# Ranking table
rows = []
for i, c in enumerate(scored[:50], 1):
rows.append({
"์ˆœ์œ„": i,
"ํ† ํ”ฝ": (c.get("cluster_label") or f"Cluster-{c['id'][:8]}")[:40],
"Attention": f"{float(c.get('attention_score', 0) or 0):.4f}",
"Density": f"{float(c.get('citation_density', 0) or 0):.4f}",
"Opportunity": f"{float(c.get('opportunity_score', 0) or 0):.4f}",
"Fanouts": c.get("fanout_count", 0),
})
df = pd.DataFrame(rows)
st.dataframe(df, use_container_width=True, hide_index=True)
# Detail expanders for top 10
st.markdown("#### Top 10 ์ƒ์„ธ")
for i, c in enumerate(scored[:10], 1):
label = c.get("cluster_label") or f"Cluster-{c['id'][:8]}"
opp = float(c.get("opportunity_score", 0) or 0)
with st.expander(f"#{i} {label} (Opportunity: {opp:.4f})", key=f"research:opp_detail:{c['id']}"):
detail_col1, detail_col2 = st.columns(2)
with detail_col1:
attn = float(c.get("attention_score", 0) or 0)
density = float(c.get("citation_density", 0) or 0)
attn_pct = _get_percentile_rank(attn, all_attns)
density_label = _interpret_density(density, median_density)
opp_label = _interpret_opportunity(opp, median_opp)
st.markdown("**Score ์ƒ์„ธ**")
st.write(f"- Attention: {attn:.4f} (์ƒ์œ„ {100 - attn_pct}%)")
st.write(f"- Density: {density:.4f} ({density_label})")
st.write(f"- Opportunity: {opp:.4f} ({opp_label})")
st.write(f"- Fanout ์ˆ˜: {c.get('fanout_count', 0)}")
if c.get("unique_questions"):
st.write(f"- ๊ด€๋ จ ์งˆ๋ฌธ ์ˆ˜: {c['unique_questions']}")
with detail_col2:
# Sample fanouts/citations with context
samples = c.get("sample_fanouts") or []
if samples:
if frame == "supply":
st.markdown("**๋Œ€ํ‘œ ์ธ์šฉ ๋ฌธ๊ตฌ** (Gemini๊ฐ€ ์‹ค์ œ๋กœ ์ธ์šฉํ•œ ํ…์ŠคํŠธ):")
else:
st.markdown("**๋Œ€ํ‘œ AI ์ถ”๊ฐ€ ์งˆ๋ฌธ** (์ด ํ† ํ”ฝ์—์„œ AI๊ฐ€ ์‹ค์ œ๋กœ ์ƒ์„ฑํ•œ ์งˆ๋ฌธ๋“ค):")
for s in samples[:5]:
st.write(f"- {s}")
if frame == "supply":
st.caption("์ด๋Ÿฐ ํ˜•ํƒœ์˜ ์ฝ˜ํ…์ธ ๋ฅผ ๋งŒ๋“ค๋ฉด Gemini ๋‹ต๋ณ€์— ์ธ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
else:
st.caption("์ด๋Ÿฐ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ์ฝ˜ํ…์ธ ๋ฅผ ๋งŒ๋“ค๋ฉด AI ๋‹ต๋ณ€์— ์ธ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
else:
st.write("์ƒ˜ํ”Œ ์—†์Œ")
# Top sources bar chart
top_sources = c.get("top_sources")
if top_sources and isinstance(top_sources, list) and len(top_sources) > 0:
st.caption("์ด ํ† ํ”ฝ์—์„œ AI๊ฐ€ ์ธ์šฉํ•œ ์ฃผ์š” ์ถœ์ฒ˜์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์— ์ž์‚ฌ ์ฝ˜ํ…์ธ ๊ฐ€ ์—†๋‹ค๋ฉด ์ง„์ถœ ๊ธฐํšŒ์ž…๋‹ˆ๋‹ค.")
domains = [s.get("host_url", "unknown") for s in top_sources[:10]]
counts = [s.get("count", 0) for s in top_sources[:10]]
fig = go.Figure(go.Bar(
x=counts,
y=domains,
orientation="h",
marker_color="#059669",
))
fig.update_layout(
title="Top ์ธ์šฉ ์ถœ์ฒ˜",
xaxis_title="์ธ์šฉ ์ˆ˜",
yaxis=dict(autorange="reversed"),
height=300,
margin=dict(l=0, r=0, t=30, b=0),
template="plotly_white",
)
st.plotly_chart(fig, use_container_width=True, config={"displayModeBar": False})
else:
st.info("์ด ํ† ํ”ฝ์€ ์•„์ง AI๊ฐ€ ํŠน์ • ์ถœ์ฒ˜๋ฅผ ์ธ์šฉํ•˜์ง€ ์•Š๊ณ  ์žˆ์–ด, ์ฝ˜ํ…์ธ  ์„ ์  ๊ธฐํšŒ๊ฐ€ ๋”์šฑ ํฝ๋‹ˆ๋‹ค.")