审计名称提及共现图
声明表面名称模式,计数段落共现,并检查阈值敏感的图摘要。
目标与运行方法
本课适合具备 Python 基础的本科生。先阅读每一步的方法与图注,再修改参数。AI 练习为可选部分,不需要 AI 账号即可完成核心课程。数据写在 notebook metadata 中,不需要另下载数据文件。请保持 notebook 已保存到当前工作文件夹。
学习任务是理解本课的研究问题、执行透明的计算、校验结果,并说明结论的边界。
环境与参数
from pathlib import Path
import base64, gzip, hashlib, json, platform
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display, HTML
import re
from collections import Counter
import networkx as nx
from IPython.display import display as _display
def display(value):
if isinstance(value,pd.DataFrame):
table=value.to_html(max_rows=12,escape=True)
style="<style>.vibeit-readable-table{max-width:100%;overflow-x:auto}.vibeit-readable-table table{width:max-content;max-width:none;border-collapse:collapse}.vibeit-readable-table td,.vibeit-readable-table th{white-space:nowrap!important;word-break:normal!important;overflow-wrap:normal!important}</style>"
return _display(HTML(style+'<div class="vibeit-readable-table">'+table+'</div>'))
return _display(value)
RECIPE_ID='hu04-mention-network'
OUTPUT_DIR=Path.cwd()/"results"/RECIPE_ID
plt.rcParams.update({"figure.dpi":120,"font.size":10,"axes.spines.top":False,"axes.spines.right":False,
"axes.prop_cycle":plt.cycler(color=["#18785f","#416aa6","#bc6541","#9974a5"])})
print("Python:",platform.python_version(),"; NumPy:",np.__version__,"; Pandas:",pd.__version__)
print("Working folder:",Path.cwd())
print("Core data mode: embedded snapshot; no network requests")
Python: 3.13.13 ; NumPy: 2.5.3 ; Pandas: 3.0.6 Working folder: /Users/zhiluo/Developer/vibeit-cookbook/cookbook/downloads Core data mode: embedded snapshot; no network requests
读取内嵌快照
def load_snapshot(recipe_id, expected_sha256, snapshot_key):
"""Read embedded data from this notebook, including a renamed copy."""
for path in sorted(Path.cwd().glob("*.ipynb")):
try:
notebook = json.loads(path.read_text(encoding="utf-8"))
metadata = notebook.get("metadata", {}).get("vibeit_cookbook", {})
blob = metadata.get("snapshots", {}).get(snapshot_key)
if metadata.get("recipe_id") != recipe_id or not blob:
continue
if blob.get("sha256") != expected_sha256:
continue
raw = gzip.decompress(base64.b64decode(blob["gzip_base64"], validate=True))
if hashlib.sha256(raw).hexdigest() != expected_sha256:
raise ValueError("Embedded snapshot checksum mismatch: " + snapshot_key)
return json.loads(raw)
except (OSError, json.JSONDecodeError):
continue
raise FileNotFoundError(
"Save/open the complete .ipynb in the active working folder. "
"The embedded snapshot could not be located for " + recipe_id)
SNAPSHOT_HASHES={'novels': 'c03763abb21b2380d2934b088f1c22a6d5a4675b17e23534e3f7343dc90084ec'}
def snapshot(key):
return load_snapshot(RECIPE_ID,SNAPSHOT_HASHES[key],key)
print("Embedded snapshots:",list(SNAPSHOT_HASHES))
Embedded snapshots: ['novels']
方法函数
def strip_ebook_wrapper(raw_text):
"""Return the analysis body while the unmodified source/complete license remain in metadata."""
text=raw_text.replace('\r\n','\n').replace('\r','\n')
start=re.search(r'\*\*\* START[^\n]*\*\*\*',text,re.I)
end=re.search(r'\*\*\* END[^\n]*\*\*\*',text,re.I)
if not start or not end or end.start()<=start.end():raise ValueError('Ebook start/end markers not found')
return text[start.end():end.start()]
print("Method ready: strip_ebook_wrapper")
Method ready: strip_ebook_wrapper
def split_novel_chapters(body):
"""Declared two-ebook rule: repeated labels resolve to last heading, excluding their TOCs."""
headings=list(re.finditer(r'(?im)^\s*chapter\s+([ivxlcdm]+|\d+)[.\]]*\s*$',body))
if not headings:raise ValueError('No supported chapter headings')
last={match.group(1).upper():match for match in headings}
chosen=sorted(last.values(),key=lambda match:match.start());chapters=[]
for j,match in enumerate(chosen):
text=body[match.end():chosen[j+1].start() if j+1<len(chosen) else len(body)]
text=re.sub(r'\[Illustration\b[^\]]*\]',' ',text,flags=re.I|re.S)
text=text.replace('_','').replace('’',"'").strip()
if len(text)<300:raise ValueError('Unexpected short chapter after declared TOC rule')
chapters.append(dict(label=match.group(1).upper(),order=j+1,text=text))
return chapters
print("Method ready: split_novel_chapters")
Method ready: split_novel_chapters
def tokenize_words(text):
"""Transparent English-letter tokenizer; punctuation/case decisions are part of the method."""
return re.findall(r"[a-z]+(?:'[a-z]+)?",text.lower().replace('’',"'"))
print("Method ready: tokenize_words")
Method ready: tokenize_words
def mention_cooccurrence(paragraphs,patterns,minimum_count=3):
"""Paragraph-level surface-pattern cooccurrence; preserve all declared entity nodes."""
if minimum_count<1:raise ValueError('Minimum cooccurrence count must be positive')
labels=list(patterns);compiled={name:re.compile(pattern,re.I) for name,pattern in patterns.items()}
matrix=np.zeros((len(labels),len(labels)),dtype=int);presence=np.zeros(len(labels),dtype=int)
for paragraph in paragraphs:
found=np.array([bool(compiled[name].search(paragraph)) for name in labels]);presence+=found
matrix+=np.outer(found,found).astype(int)
np.fill_diagonal(matrix,0);graph=nx.Graph();graph.add_nodes_from(labels)
for i in range(len(labels)):
for j in range(i+1,len(labels)):
if matrix[i,j]>=minimum_count:graph.add_edge(labels[i],labels[j],paragraph_count=int(matrix[i,j]))
return dict(graph=graph,labels=labels,matrix=matrix,paragraph_presence=presence)
print("Method ready: mention_cooccurrence")
Method ready: mention_cooccurrence
数据与模型边界
快照保留完整未修改来源电子书及原许可。章节分析使用《傲慢与偏见》61 章及《弗兰肯斯坦》24 章;前言和后者书信框架不在本次声明范围。英文字母分词是方法选择,不是语言无关定义。词频、表面模式共现和 SVD 轴不能独立证明作者意图、人物关系或情感。
分步分析
1. 声明别名模式及段落单位
在《傲慢与偏见》中使用表面模式。Darcy 标为姓氏规则,可能出现讨论或家族提及,不确定唯一人物身份。共现指同一处理段落中两模式均出现。
novels=snapshot("novels");corpus={};chapter_rows=[]
for book in novels["books"]:
chapters=split_novel_chapters(strip_ebook_wrapper(book["raw_text"]))
tokens=tokenize_words(" ".join(chapter["text"] for chapter in chapters))
corpus[book["id"]]={"title":book["title"],"author":book["author"],"chapters":chapters,"tokens":tokens}
for chapter in chapters:chapter_rows.append({"book_id":book["id"],"book":book["title"],"chapter":chapter["order"],"source_label":chapter["label"],"characters":len(chapter["text"]),"tokens":len(tokenize_words(chapter["text"])),"text":chapter["text"]})
chapter_table=pd.DataFrame(chapter_rows)
assert len(corpus[1342]["chapters"])==61 and len(corpus[84]["chapters"])==24
print(novels["source_notice"])
print("Declared chapter counts:",{b:len(corpus[b]["chapters"]) for b in corpus})
display(chapter_table.drop(columns="text").head(8))
patterns={"Elizabeth / Lizzy":r"\b(?:elizabeth|lizzy|eliza)\b","Jane":r"\bjane\b","Darcy surname":r"\bdarcy\b","Bingley surname":r"\bbingley\b","Mr Bennet":r"\bmr\.?\s+bennet\b","Mrs Bennet":r"\bmrs\.?\s+bennet\b","Lydia":r"\blydia\b","Wickham":r"\bwickham\b"}
paragraphs=[part for chapter in corpus[1342]["chapters"] for part in re.split(r"\n\s*\n",chapter["text"]) if part.strip()]
MINIMUM=3;network=mention_cooccurrence(paragraphs,patterns,MINIMUM);graph=network["graph"]
print("Processed paragraphs:",len(paragraphs),"; declared entities:",len(patterns),"; minimum shared paragraphs:",MINIMUM)
display(pd.DataFrame({"pattern":list(patterns),"regex":list(patterns.values()),"paragraph_presence":network["paragraph_presence"]}))
These source ebooks are available free of charge. Complete unmodified source texts and their licenses are included in this snapshot. Project Gutenberg is acknowledged as the transcription source; its name is not used as a course title or endorsement.
Declared chapter counts: {1342: 61, 84: 24}
| book_id | book | chapter | source_label | characters | tokens | |
|---|---|---|---|---|---|---|
| 0 | 1342 | Pride and Prejudice | 1 | I | 4490 | 853 |
| 1 | 1342 | Pride and Prejudice | 2 | II | 4293 | 799 |
| 2 | 1342 | Pride and Prejudice | 3 | III | 9560 | 1704 |
| 3 | 1342 | Pride and Prejudice | 4 | IV | 5931 | 1064 |
| 4 | 1342 | Pride and Prejudice | 5 | V | 5246 | 962 |
| 5 | 1342 | Pride and Prejudice | 6 | VI | 13023 | 2353 |
| 6 | 1342 | Pride and Prejudice | 7 | VII | 11263 | 2000 |
| 7 | 1342 | Pride and Prejudice | 8 | VIII | 10993 | 1936 |
Processed paragraphs: 2096 ; declared entities: 8 ; minimum shared paragraphs: 3
| pattern | regex | paragraph_presence | |
|---|---|---|---|
| 0 | Elizabeth / Lizzy | \b(?:elizabeth|lizzy|eliza)\b | 636 |
| 1 | Jane | \bjane\b | 229 |
| 2 | Darcy surname | \bdarcy\b | 344 |
| 3 | Bingley surname | \bbingley\b | 249 |
| 4 | Mr Bennet | \bmr\.?\s+bennet\b | 82 |
| 5 | Mrs Bennet | \bmrs\.?\s+bennet\b | 136 |
| 6 | Lydia | \blydia\b | 135 |
| 7 | Wickham | \bwickham\b | 141 |
2. 解读阈值共现图
边计声明规则下的共享段落,布局固定种子,边宽编码数量。此图不是已核实社会关系网,也不证明人物相遇。即便无边,保留全部声明节点。
position=nx.spring_layout(graph,seed=42,weight="paragraph_count")
fig,ax=plt.subplots(figsize=(9,6));width=[1+np.log1p(graph[a][b]["paragraph_count"]) for a,b in graph.edges()]
nx.draw_networkx(graph,pos=position,ax=ax,node_size=1500,font_size=8,width=width,node_color="#b4d4ed")
ax.set_title("Surface-pattern cooccurrence; threshold = 3 shared paragraphs");ax.axis("off");plt.tight_layout();plt.show()
3. 检查底层计数矩阵
矩阵为共享段落数量,对角线明确设零。图把计数依据与绘制网络分开;高数量可能来自高提及率,不是人际重要性。
fig,ax=plt.subplots(figsize=(8,6));im=ax.imshow(network["matrix"],cmap="Blues")
ax.set_xticks(range(8),network["labels"],rotation=55,ha="right",fontsize=8);ax.set_yticks(range(8),network["labels"],fontsize=8)
fig.colorbar(im,ax=ax,label="Shared processed paragraphs");ax.set_title("Declared alias-rule counts");plt.tight_layout();plt.show()
4. 比较阈值并保留声明节点
按共享段落一、三、五、十重建。阈值改变边,节点集固定。中心性依赖这些选择,不是叙事事实。
sensitivity=[]
for minimum in [1,3,5,10]:
trial=mention_cooccurrence(paragraphs,patterns,minimum)["graph"]
sensitivity.append({"minimum_shared_paragraphs":minimum,"nodes":trial.number_of_nodes(),"edges":trial.number_of_edges(),"isolates":len(list(nx.isolates(trial)))})
sensitivity=pd.DataFrame(sensitivity)
fig,ax=plt.subplots(figsize=(7,4));ax.plot(sensitivity.minimum_shared_paragraphs,sensitivity.edges,"o-")
ax.set(xlabel="Minimum shared paragraphs",ylabel="Retained edges",title="Threshold-sensitive cooccurrence representation");plt.tight_layout();plt.show()
5. 核对数量并导出可检视规则
检查对称计数、完整节点及保留边单调性。导出模式、数量和 GraphML,保留孤立点,并说明姓氏歧义需回到语境核查。
assert graph.number_of_edges()>0 and network["paragraph_presence"].sum()>0
assert np.array_equal(network["matrix"],network["matrix"].T) and np.all(np.diag(network["matrix"])==0)
assert set(graph)==set(patterns) and sensitivity.nodes.eq(8).all() and np.all(np.diff(sensitivity.edges)<=0)
OUTPUT_DIR.mkdir(parents=True,exist_ok=True);nx.write_graphml(graph,OUTPUT_DIR/"surface-mention-cooccurrence.graphml")
pd.DataFrame(network["matrix"],index=network["labels"],columns=network["labels"]).to_csv(OUTPUT_DIR/"shared-paragraph-counts.csv")
sensitivity.to_csv(OUTPUT_DIR/"threshold-sensitivity.csv",index=False)
(OUTPUT_DIR/"alias-rules.json").write_text(json.dumps(patterns,indent=2))
print("Count/node checks passed; interpretation remains conditional on alias rules")
Count/node checks passed; interpretation remains conditional on alias rules
自主练习与参考答案
- 姓氏共现确定社会关系吗?
- 为何保留声明孤立点?
参考解释。 表面别名可能混家族/讨论。孤立点记录范围,中心性依赖模式、段落和阈值。
先保留当前 notebook 副本,再修改参数。把新图与原图一起比较,并记录改变了哪些输入、哪些计算和哪些结论。
可选的 AI 编程练习
提示词 1
仅用 re、NumPy、Pandas、Matplotlib、NetworkX 比较阈值一、三、十,固定节点/布局。
提示词 2
同包检查 Darcy/Bingley 歧义语境,修订规则另存。
人工验收。 对称计数、八节点、无自身边,不自动推关系/意图。
检查修改后的源代码,再手动运行受影响单元及后续校验。要求助手解释修改的目的、使用的包和数据来源,并用实际输出核对解释。
可选联网扩展
RUN_ONLINE_UPDATE=False
if RUN_ONLINE_UPDATE:
import requests
try:
response=requests.get('https://www.gutenberg.org/ebooks/1342',timeout=20)
response.raise_for_status()
print("Source:",response.url)
print(response.text[:700])
except requests.RequestException as error:
print("Online extension unavailable:",type(error).__name__,str(error)[:160])
else:
print("Online extension skipped; embedded core data unchanged")
Online extension skipped; embedded core data unchanged
来源、快照与下一步
Historical novel 1342 — Original novel public domain in USA; complete source ebook and its Project Gutenberg license retained; free distribution and acknowledgment links; retrieved 2026-10-09. SHA-256
3f6bb9d6f78e0293b56acd4714dd68cb7d6d1d293402031ce9d5a216bcaf9d75. Preserve unmodified UTF-8 source including ebook headers and complete license; preprocessing occurs visibly in the notebook.Historical novel 84 — Original novel public domain in USA; complete source ebook and its Project Gutenberg license retained; free distribution and acknowledgment links; retrieved 2026-10-09. SHA-256
7810cd483cffcf2cc8a1d8f0d5807931e69d4f48cd14149b8c76f88af82fead3. Preserve unmodified UTF-8 source including ebook headers and complete license; preprocessing occurs visibly in the notebook.
下一步。 在保留本课的数据检查、参数记录和结果校验之后,将相同方法用于自己的公开或已获授权数据。记录研究设计与方法限制,不把示例结果当成新实验的验证。完整数据许可与生成说明见 cookbook 网页。