在互联网行为分析中,"社交网络分析"不一定只能依赖好友、关注、私信或转发关系。很多时候,数据里并没有显式的社交边,但用户的网页访问、应用使用、停留时长和活跃节奏,本身就能反映出相似的兴趣圈层。
本项目中的"社交网络分析核心"正是基于这个思路:从清洗后的上网行为日志中抽取用户共同访问的网页域名和共同使用的应用程序,构建"用户-资源-用户"的行为相似网络,再识别核心用户、兴趣社群和跨圈桥接用户。
一、分析背景
1.1 这里的"社交网络"是什么
传统社交网络一般描述真实互动关系,例如:
- A 关注了 B
- A 给 B 发消息
- A 评论或转发了 B 的内容
- A 和 B 是好友
但本项目的数据来源是互联网行为日志,核心字段是用户、访问时间、停留时长、网页域名、进程名称等。因此这里构建的不是直接社交关系,而是:
基于共同访问资源推断出来的用户行为相似网络。
也就是说,如果两个用户都经常访问 user.qzone.qq.com,都使用 qq.exe,或者都长时间使用某些办公、视频、游戏类应用,那么他们之间会形成一条相似关系边。
这类网络适合回答以下问题:
- 哪些用户在行为网络中最核心?
- 用户是否自然形成了不同兴趣社群?
- 哪些用户连接了多个兴趣圈层?
- 哪些用户活跃度很低,需要唤醒?
- 哪些用户行为强度异常高,需要复核?
二、数据与预处理
2.1 从清洗后的行为事件开始
分析使用的主要数据是:
text
互联网行为分析/cleaned/behavior_events.csv
互联网行为分析/data/demographic.csv
其中 behavior_events.csv 是清洗后的行为事件表,包含用户访问网页或使用应用的事件记录;demographic.csv 用于补充性别、学历、职业、地区等用户画像字段。
脚本中的路径定义如下:
python
CORE_DIR = Path(__file__).resolve().parents[1]
PROJECT_DIR = CORE_DIR.parent
CLEANED_DIR = PROJECT_DIR / "cleaned"
DATA_DIR = PROJECT_DIR / "data"
EVENTS_CSV = CLEANED_DIR / "behavior_events.csv"
DEMOGRAPHIC_CSV = DATA_DIR / "demographic.csv"
这样做的好处是脚本可以稳定地从项目目录读取数据,不依赖当前终端所在路径。
2.2 清洗无效行为
行为日志里会出现很多不适合用于兴趣分析的记录,例如系统进程、浏览器空壳、无效停留时长和异常长停留。这些记录如果直接用于建图,会把网络关系扭曲。
脚本先定义了两类需要过滤的应用:
python
NOISE_APPS = {
"explorer.exe", "svchost.exe", "rundll32.exe", "ctfmon.exe",
"dwm.exe", "conhost.exe", "services.exe", "winlogon.exe",
"?", "-", "unknown", "null",
}
BROWSER_APPS = {
"360chrome.exe", "chrome.exe", "iexplore.exe", "firefox.exe",
"qqbrowser.exe", "sogouexplorer.exe", "opera.exe", "safari.exe",
}
过滤逻辑主要有四点:
- 没有用户 ID 的记录不参与分析。
duration_invalid_flag为 1 的无效停留记录不参与分析。- 停留时长小于等于 0 的记录,用 2 秒作为极小默认值。
- 异常长停留最多按 1800 秒计算,避免挂机行为放大边权。
核心代码如下:
python
invalid = parse_int(cell(row, "duration_invalid_flag"))
large = parse_int(cell(row, "duration_large_flag"))
if invalid:
continue
duration = parse_float(cell(row, "duration_seconds"), 0.0)
if duration <= 0:
duration = 2.0
if large:
duration = min(duration, 1800.0)
duration = min(duration, 1800.0)
这一步的目的不是"删除一切异常",而是让网络边权更稳健,减少系统噪声和挂机时长对结果的影响。
2.3 把网页和应用统一成"行为资源"
用户的行为来源分为两类:
- 网页访问:使用网页域名作为资源。
- 应用使用:使用非系统、非浏览器进程名作为资源。
例如:
text
web:user.qzone.qq.com
web:www.baidu.com
app:qq.exe
app:excel.exe
app:winword.exe
网页域名会先标准化:
python
def normalize_domain(raw: str) -> str:
value = (raw or "").strip().lower()
value = re.sub(r"^https?://", "", value)
value = value.split("/")[0].split(":")[0]
value = value.removeprefix("www.")
return value
资源生成逻辑如下:
python
is_web = parse_int(cell(row, "is_web_event"))
domain = normalize_domain(cell(row, "url_domain") or cell(row, "url_raw") or "")
process = (cell(row, "process_name") or "").strip().lower()
if is_web and domain:
resource = f"web:{domain}"
elif process and process not in NOISE_APPS and process not in BROWSER_APPS:
resource = f"app:{process}"
else:
skipped_noise += 1
continue
统一成资源以后,网页和应用就可以放到同一套网络模型中分析。
2.4 给资源打上行为类别标签
为了让社群结果可解释,脚本还对资源进行粗粒度分类,例如社交沟通、搜索导航、视频娱乐、新闻资讯、电商消费、学习知识、邮箱办公、游戏休闲、软件工具等。
分类规则使用关键词匹配:
python
CATEGORY_RULES = [
("社交沟通", ("qq.", "qzone", "weibo", "renren", "douban", "tieba", "bbs")),
("搜索导航", ("baidu", "google", "sogou", "so.com", "bing", "hao123")),
("视频娱乐", ("youku", "tudou", "iqiyi", "pptv", "bilibili", "video")),
("新闻资讯", ("sina", "sohu", "163.com", "people", "ifeng", "news")),
("电商消费", ("taobao", "tmall", "jd.com", "amazon", "alipay")),
("学习知识", ("wikipedia", "baike", "wenku", "edu", "csdn", "zhihu")),
("邮箱办公", ("mail", "outlook", "office", "wps", "word", "excel")),
("游戏休闲", ("game", "4399", "qqgame", "steam", "lol")),
]
def category_for_resource(resource: str) -> str:
text = resource.lower()
for category, needles in CATEGORY_RULES:
if any(needle in text for needle in needles):
return category
if resource.startswith("app:"):
return "软件工具"
return "其他访问"
这一步很重要。网络算法可以告诉我们"谁和谁相似",但类别标签可以帮助解释"为什么相似"。
三、网络构建
3.1 构建用户-资源二部图
脚本先不直接构建用户之间的关系,而是构建一个二部图:
text
用户 -> 资源 -> 停留时长
例如:
text
用户 A -> qq.exe -> 5000 秒
用户 A -> user.qzone.qq.com -> 3000 秒
用户 B -> qq.exe -> 4200 秒
用户 B -> user.qzone.qq.com -> 2800 秒
如果两个用户共享多个资源,就说明他们有相似的上网行为。
核心统计结构如下:
python
user_resource = defaultdict(lambda: defaultdict(float))
user_resource_events = defaultdict(lambda: defaultdict(int))
user_category = defaultdict(lambda: defaultdict(float))
user_events = Counter()
user_days = defaultdict(set)
resource_users = defaultdict(lambda: defaultdict(float))
每读入一条有效行为记录,就更新这些统计量:
python
user_resource[uid][resource] += duration
user_resource_events[uid][resource] += 1
user_category[uid][category] += duration
user_events[uid] += 1
resource_users[resource][uid] += duration
其中最关键的是两个方向:
user_resource:从用户看资源,用于刻画用户兴趣。resource_users:从资源看用户,用于后续把共同资源投影成用户关系。
3.2 把二部图投影为用户-用户相似网络
有了 resource_users 以后,就可以生成用户之间的边。
脚本先过滤资源:
python
if 2 <= n_users <= 120 and total_duration >= 30:
resource_stats.append((total_duration, n_users, resource))
这里有两个考虑:
- 只有 1 个用户访问的资源无法产生用户关系。
- 覆盖用户太多的资源可能太泛,例如常见入口或通用工具,区分度较低。
然后选出停留强度较高的前 360 个资源:
python
resource_stats.sort(reverse=True)
selected_resources = {resource for _, _, resource in resource_stats[:360]}
对每个资源,取使用时长最高的前 80 个用户,两两组合生成边:
python
for (u1, w1), (u2, w2) in itertools.combinations(users, 2):
pair = tuple(sorted((u1, u2)))
edge_weights[pair] += math.sqrt(max(w1, 1.0) * max(w2, 1.0))
edge_categories[pair][category] += 1
边权公式是:
text
边权 += sqrt(用户1在资源上的停留时长 * 用户2在资源上的停留时长)
这个公式的直觉是:只有两个人都对同一资源有较强停留时,边权才会明显增加。
最后再过滤较弱边:
python
threshold = max(percentile(weights, 0.60), 20.0)
并保留权重最高的前 1800 条边:
python
edges.sort(key=lambda item: item["weight"], reverse=True)
edges = edges[:1800]
这一步得到的就是用户相似网络。
四、网络指标与用户分层
4.1 计算网络中心性
网络建好以后,脚本会计算三个重要指标。
4.1.1 度数 degree
度数表示一个用户连接了多少个相似用户:
python
degrees = {node: len(adjacency[node]) for node in adjacency}
度数越高,说明这个用户和更多用户有共同兴趣或共同资源。
4.1.2 加权度 weightedDegree
加权度表示用户所有连接强度之和:
python
weighted_degrees = {
node: sum(adjacency[node].values())
for node in adjacency
}
它比普通度数更重视连接质量。一个用户连接人数不多,但如果共同资源停留很强,也可能有较高加权度。
4.1.3 加权 PageRank
PageRank 用于识别网络中的核心扩散用户。脚本实现的是加权版本:
python
def weighted_pagerank(adjacency, damping=0.85, iterations=45):
nodes = sorted(adjacency)
n = len(nodes)
if not nodes:
return {}
ranks = {node: 1.0 / n for node in nodes}
out_weight = {node: sum(adjacency[node].values()) for node in nodes}
base = (1.0 - damping) / n
for _ in range(iterations):
new_ranks = {node: base for node in nodes}
for node in nodes:
if out_weight[node] <= 0:
continue
share = damping * ranks[node] / out_weight[node]
for nb, weight in adjacency[node].items():
new_ranks[nb] += share * weight
ranks = new_ranks
return ranks
PageRank 高的用户,不只是连接多,而且连接对象本身也比较重要。
4.2 用标签传播识别兴趣社群
社群识别使用的是加权标签传播算法。初始时,每个用户都有自己的标签;之后每个用户不断观察邻居标签,把自己改成邻居中权重最高的标签。
核心代码如下:
python
def label_propagation(adjacency, max_iter=35):
labels = {node: node for node in adjacency}
for _ in range(max_iter):
changed = 0
for node in sorted(adjacency, key=lambda n: (-len(adjacency[n]), n)):
score = Counter()
for nb, weight in adjacency[node].items():
score[labels[nb]] += weight
if not score:
continue
best_score = max(score.values())
best_labels = sorted(
label for label, value in score.items()
if value == best_score
)
best = best_labels[0]
if labels[node] != best:
labels[node] = best
changed += 1
if not changed:
break
raw_counts = Counter(labels.values())
ordered = [label for label, _ in raw_counts.most_common()]
remap = {label: f"C{i + 1}" for i, label in enumerate(ordered)}
return {node: remap[label] for node, label in labels.items()}
最终得到的社群编号类似:
text
C1, C2, C3, ...
脚本还会统计每个社群的代表资源、主导类别和平均行为时长,用于解释社群画像。
4.3 识别跨圈桥接用户
有些用户不一定是 PageRank 最高的核心用户,但他们连接了多个不同社群。这类用户在传播、推荐、异常路径解释中很有价值。
脚本通过邻居社群分布的熵来计算桥接分:
python
counts = Counter(communities.get(nb, "C0") for nb in adjacency[node])
total = sum(counts.values())
entropy = -sum(
(count / total) * math.log(count / total + 1e-12)
for count in counts.values()
)
bridge_scores[node] = entropy * min(1.0, degrees[node] / max(deg50, 1.0))
如果一个用户的邻居都来自同一个社群,熵较低;如果邻居分散在多个社群,熵较高。
所以桥接分高的用户,往往具有"跨圈连接"能力。
4.4 用户分层
最终用户会被分成五类:
| 分层 | 判断逻辑 | 业务含义 |
|---|---|---|
| 核心扩散层 | PageRank 高且加权度高 | 适合作为信息触达和内容扩散对象 |
| 跨圈桥接层 | 桥接分高且连接数不低 | 连接多个兴趣社群,适合跨圈传播 |
| 兴趣社群层 | 行为稳定但不是核心或桥接 | 适合按兴趣主题做精细化运营 |
| 低活跃唤醒层 | 活跃天数少或总时长低 | 适合轻量提醒、入口优化、召回 |
| 高强度复核层 | 事件数或总时长异常高 | 需要复核是否挂机、批量访问或特殊任务 |
对应代码如下:
python
if total_duration >= duration95 or user_events[uid] >= event95:
segment = "高强度复核层"
elif pr >= pr90 and weighted_degree >= wd80:
segment = "核心扩散层"
elif bridge >= bridge85 and degree >= deg50:
segment = "跨圈桥接层"
elif len(user_days[uid]) <= 2 or total_duration <= duration20:
segment = "低活跃唤醒层"
else:
segment = "兴趣社群层"
这套规则不是简单按访问量排名,而是结合了行为强度、网络位置和跨社群能力。
五、结果输出与可视化
5.1 结果如何解读
脚本运行后会输出节点表和边表。
节点表 social_network_nodes.csv 中,每一行是一个用户,核心字段包括:
text
id
community
segment
topCategory
degree
weightedDegree
pagerank
bridgeScore
events
activeDays
durationMinutes
gender
edu
job
province
city
边表 social_network_edges.csv 中,每一行是两个用户之间的相似关系:
text
source
target
weight
category
例如一条边可以理解为:
source 用户和 target 用户因为共同使用某类资源而形成行为相似关系,weight 越大,说明共同资源停留强度越高。
报告 social_network_report.md 会汇总:
- 原始事件数
- 有效行为数
- 活跃用户数
- 入网用户数
- 用户相似边数
- 网络密度
- 平均度
- 最大连通分量占比
- 社群数量
- 分流结果
- 关键用户
- 资源类别分布
5.2 可视化展示
网页端在:
text
互联网行为分析/社交网络分析核心/web/index.html
页面直接读取:
text
web/data.js
因此不需要后端服务,直接打开 HTML 即可查看。
可视化主要展示:
- 社交网络图:节点大小表示中心性,颜色表示分层。
- 分流流程图:展示从行为日志到用户分层的路径。
- 核心用户排名:按 PageRank、加权度、桥接分展示关键用户。
- 社群规模和行为类别:解释不同兴趣社群的主题。
- 24 小时行为节律:观察用户活跃时间分布。
5.3 运行方式
在 PowerShell 中进入核心目录:
powershell
cd "C:\Users\kishi\Desktop\商业数据分析\期末项目\互联网行为分析\社交网络分析核心"
python .\scripts\analyze_social_network.py
运行完成后打开:
text
web/index.html
即可查看网页可视化结果。
六、局限与总结
6.1 方法局限
这套方法的核心假设是:
共同访问资源可以反映用户之间的行为相似性。
因此它适合分析兴趣圈层、内容触达、相似用户、行为分流和异常复核,但不能直接解释为真实好友关系。
如果后续数据中加入评论、转发、私信、好友列表、关注关系等字段,就可以进一步升级为真实交互网络,并计算有向边、入度、出度、传播路径和互动强度。
6.2 小结
这个模块的关键价值在于:它把海量、分散的上网行为日志转换成了可解释的网络结构。
分析链路可以概括为:
text
清洗行为日志
-> 抽取网页域名和应用进程
-> 构建用户-资源二部图
-> 投影为用户-用户相似网络
-> 计算 PageRank、加权度和桥接分
-> 标签传播识别兴趣社群
-> 输出用户分层和可视化结果
从业务角度看,它不仅能告诉我们"谁访问得多",还能告诉我们"谁处在网络中心""谁连接多个圈层""哪些用户属于同一兴趣群体"。这也是社交网络分析相比普通统计分析更有价值的地方。