160 lines
5.4 KiB
Python
160 lines
5.4 KiB
Python
"""
|
|
五维分析引擎 + 自动状态分类
|
|
|
|
维度: time(时效) / heat(热度) / content(内容) / propagation(传播) / audience(受众)
|
|
状态: emerging → rising → hot → cooling → expired
|
|
"""
|
|
import json, math
|
|
from datetime import datetime, timedelta
|
|
from appPublic.uniqueID import getID
|
|
from sqlor.dbpools import DBPools
|
|
|
|
def now_str():
|
|
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
DIM_NAMES = {
|
|
'time': '时效维度',
|
|
'heat': '热度指标',
|
|
'content': '内容属性',
|
|
'propagation': '传播路径',
|
|
'audience': '受众画像',
|
|
}
|
|
|
|
async def analyze_item(item):
|
|
"""分析单条热点,返回 (new_status, dimensions_dict, detail)"""
|
|
now = datetime.now()
|
|
try:
|
|
first_seen = datetime.strptime(str(item.get('first_seen', '')), '%Y-%m-%d %H:%M:%S')
|
|
except Exception:
|
|
first_seen = now
|
|
|
|
hours_alive = max(0.1, (now - first_seen).total_seconds() / 3600)
|
|
heat = float(item.get('heat_score', 0))
|
|
engagement = int(item.get('engagement_count', 0))
|
|
comments = int(item.get('comment_count', 0))
|
|
shares = int(item.get('share_count', 0))
|
|
|
|
# ---- 五维评分 (0-100) ----
|
|
dims = {}
|
|
|
|
# 时效: 越新越高
|
|
dims['time'] = 95 if hours_alive < 1 else 85 if hours_alive < 6 else \
|
|
70 if hours_alive < 24 else 50 if hours_alive < 72 else \
|
|
30 if hours_alive < 168 else 10
|
|
|
|
# 热度: 绝对值
|
|
dims['heat'] = 95 if heat > 1e4 else 85 if heat > 5e3 else \
|
|
70 if heat > 1e3 else 55 if heat > 500 else \
|
|
35 if heat > 100 else 15
|
|
|
|
# 内容: 标题长度 + 摘要丰富度 + 标签
|
|
title_len = len(item.get('title', ''))
|
|
summary_len = len(item.get('summary', ''))
|
|
dims['content'] = min(100, (20 if title_len > 15 else 10) +
|
|
(30 if summary_len > 100 else 15) +
|
|
(25 if item.get('tags') else 0) +
|
|
(25 if item.get('category') else 0))
|
|
|
|
# 传播: 互动总量
|
|
total_eng = engagement + comments * 2 + shares * 3
|
|
dims['propagation'] = 95 if total_eng > 1e4 else 80 if total_eng > 5e3 else \
|
|
60 if total_eng > 1e3 else 35 if total_eng > 100 else 10
|
|
|
|
# 受众: 互动率
|
|
rate = total_eng / heat if heat > 0 else 0
|
|
dims['audience'] = 90 if rate > 0.5 else 70 if rate > 0.2 else \
|
|
45 if rate > 0.05 else 25 if total_eng > 0 else 5
|
|
|
|
# ---- 状态分类 ----
|
|
decay = math.exp(-hours_alive / 168) # 7天半衰期
|
|
adjusted_heat = heat * decay
|
|
velocity = round(heat / hours_alive, 2)
|
|
|
|
if hours_alive > 336: # >14天
|
|
new_status = 'expired'
|
|
elif hours_alive > 168: # 7-14天
|
|
new_status = 'cooling'
|
|
elif adjusted_heat > 5000:
|
|
new_status = 'hot'
|
|
elif adjusted_heat > 500:
|
|
new_status = 'rising' if velocity > 50 else 'emerging'
|
|
elif adjusted_heat > 100:
|
|
new_status = 'rising' if velocity > 100 else 'emerging'
|
|
else:
|
|
new_status = 'emerging'
|
|
|
|
detail = {
|
|
'hours_alive': round(hours_alive, 1),
|
|
'adjusted_heat': round(adjusted_heat, 2),
|
|
'heat_velocity': velocity,
|
|
'total_engagement': total_eng,
|
|
'decay_factor': round(decay, 4),
|
|
}
|
|
|
|
return new_status, dims, detail
|
|
|
|
|
|
async def save_analysis(item_id, dims, detail, now=None):
|
|
"""保存/更新五维分析记录"""
|
|
if now is None:
|
|
now = now_str()
|
|
db = DBPools()
|
|
|
|
async with db.sqlorContext('sage') as sor:
|
|
for dim, score in dims.items():
|
|
analysis_data = json.dumps({
|
|
'dimension': dim,
|
|
'dimension_cn': DIM_NAMES.get(dim, dim),
|
|
'score': score,
|
|
'detail': detail,
|
|
}, ensure_ascii=False)
|
|
|
|
old = await sor.sqlExe(
|
|
"select id from hotspot_analysis where item_id=${iid}$ and dimension=${dim}$",
|
|
{'iid': item_id, 'dim': dim})
|
|
if old:
|
|
await sor.U('hotspot_analysis', {
|
|
'id': old[0]['id'], 'score': score,
|
|
'analysis_data': analysis_data, 'analyzed_at': now,
|
|
})
|
|
else:
|
|
await sor.C('hotspot_analysis', {
|
|
'id': getID(), 'item_id': item_id,
|
|
'dimension': dim, 'score': score,
|
|
'analysis_data': analysis_data, 'analyzed_at': now,
|
|
})
|
|
|
|
|
|
async def run_analysis(limit=500):
|
|
"""批量分析最近的热点 (默认500条)"""
|
|
db = DBPools()
|
|
now = now_str()
|
|
results = {'analyzed': 0, 'status_changes': 0}
|
|
|
|
async with db.sqlorContext('sage') as sor:
|
|
items = await sor.sqlExe(
|
|
"select * from hotspot_item order by first_seen desc limit ${n}$",
|
|
{'n': limit})
|
|
|
|
for item in items:
|
|
current_status = item.get('status', 'emerging')
|
|
new_status, dims, detail = await analyze_item(item)
|
|
|
|
await save_analysis(item['id'], dims, detail, now)
|
|
|
|
if new_status != current_status:
|
|
results['status_changes'] += 1
|
|
|
|
async with db.sqlorContext('sage') as sor:
|
|
await sor.U('hotspot_item', {
|
|
'id': item['id'],
|
|
'heat_score': round(detail['adjusted_heat'], 2),
|
|
'heat_velocity': detail['heat_velocity'],
|
|
'status': new_status,
|
|
'last_updated': now,
|
|
})
|
|
|
|
results['analyzed'] += 1
|
|
|
|
return results
|