init: KG_ICH 项目初始化

- data/: 非遗地理编码数据(GIS shapefile + CSV)
- dofile/kg_project/: 知识图谱构建代码(纳入主仓库)
- dofile/visulization/: 可视化数据与路线图
- officefile/: 文献、草稿、bib 文档
- officefile/latex/: Overleaf 同步目录(独立管理,不纳入)
- output/: 输出目录
- logs/: 日志目录
This commit is contained in:
2026-05-30 00:52:36 +08:00
commit 834cad729f
44 changed files with 8586 additions and 0 deletions
+375
View File
@@ -0,0 +1,375 @@
"""
数据预处理脚本 - 读取黑龙江非遗Excel数据
"""
import pandas as pd
import json
from pathlib import Path
from typing import Dict, List, Any
import logging
class ExcelDataReader:
"""Excel数据读取器"""
def __init__(self, excel_path: str):
"""
初始化数据读取器
Args:
excel_path: Excel文件路径
"""
self.excel_path = Path(excel_path)
self.data = None
self.logger = self._setup_logger()
def _setup_logger(self):
"""设置日志"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
return logging.getLogger(__name__)
def read_excel(self) -> pd.DataFrame:
"""
读取Excel文件
Returns:
DataFrame: 数据框
"""
try:
self.logger.info(f"开始读取Excel文件: {self.excel_path}")
# 读取Excel文件
self.data = pd.read_excel(self.excel_path)
self.logger.info(f"成功读取 {len(self.data)} 行数据")
self.logger.info(f"列名: {list(self.data.columns)}")
# 显示前5行
self.logger.info("\n前5行数据:")
self.logger.info(self.data.head())
return self.data
except Exception as e:
self.logger.error(f"读取Excel文件失败: {str(e)}")
raise
def analyze_data(self) -> Dict[str, Any]:
"""
分析数据
Returns:
Dict: 分析结果
"""
if self.data is None:
raise ValueError("请先读取Excel文件")
analysis = {
"total_records": len(self.data),
"columns": list(self.data.columns),
"column_types": {col: str(dtype) for col, dtype in self.data.dtypes.items()},
"missing_values": self.data.isnull().sum().to_dict(),
"statistics": {}
}
# 分析类别分布
if '类别' in self.data.columns:
category_counts = self.data['类别'].value_counts()
analysis['category_distribution'] = category_counts.to_dict()
# 分析级别分布
if '项目级别' in self.data.columns:
level_counts = self.data['项目级别'].value_counts()
analysis['level_distribution'] = level_counts.to_dict()
# 分析地域分布
if '项目申报单位/地区' in self.data.columns:
location_counts = self.data['项目申报单位/地区'].value_counts()
analysis['location_distribution'] = location_counts.head(20).to_dict()
# 传承人覆盖率
if '代表性传承人' in self.data.columns:
has_inheritor = self.data['代表性传承人'].notna().sum()
analysis['inheritor_coverage'] = {
"total": len(self.data),
"has_inheritor": int(has_inheritor),
"coverage_rate": float(has_inheritor / len(self.data) * 100)
}
return analysis
def clean_data(self) -> pd.DataFrame:
"""
清洗数据
Returns:
DataFrame: 清洗后的数据
"""
if self.data is None:
raise ValueError("请先读取Excel文件")
self.logger.info("开始清洗数据")
# 去除空行
original_len = len(self.data)
self.data = self.data.dropna(how='all')
self.logger.info(f"去除空行: {original_len} -> {len(self.data)}")
# 填充缺失值
for col in self.data.columns:
if self.data[col].dtype == 'object':
self.data[col] = self.data[col].fillna('')
else:
self.data[col] = self.data[col].fillna(0)
# 去重(基于项目名称、项目批次、项目保护单位、代表性传承人四个字段)
dedup_cols = ['项目名称', '项目批次', '项目保护单位', '代表性传承人']
existing_cols = [col for col in dedup_cols if col in self.data.columns]
if existing_cols:
before_dedup = len(self.data)
self.data = self.data.drop_duplicates(subset=existing_cols, keep='first')
duplicate_count = before_dedup - len(self.data)
dedup_rate = (duplicate_count / before_dedup * 100) if before_dedup > 0 else 0
self.logger.info(f"去重(基于{len(existing_cols)}个字段: {', '.join(existing_cols)}: {before_dedup} -> {len(self.data)} (删除{duplicate_count}条,去重率{dedup_rate:.1f}%)")
return self.data
def convert_to_kg_format(self) -> List[Dict[str, Any]]:
"""
转换为知识图谱格式
Returns:
List[Dict]: 知识图谱节点列表
"""
if self.data is None:
raise ValueError("请先读取Excel文件")
self.logger.info("转换为知识图谱格式")
kg_nodes = []
for idx, row in self.data.iterrows():
try:
# 创建非遗项目节点
project_node = {
"id": f"ICH-{idx:04d}",
"type": "ICH_Project",
"properties": {
"project_id": f"ICH-{idx:04d}",
"name": str(row.get('项目名称', '')),
"level": str(row.get('项目级别', '')),
"batch": str(row.get('批次号', '')),
"category": str(row.get('类别', '')),
"declaration_area": str(row.get('项目申报单位/地区', '')),
"description": str(row.get('备注', '')),
"source_row": idx + 2 # Excel行号(从2开始,第一行是表头)
}
}
kg_nodes.append(project_node)
except Exception as e:
self.logger.warning(f"转换第{idx}行数据失败: {str(e)}")
continue
self.logger.info(f"成功转换 {len(kg_nodes)} 个节点")
return kg_nodes
def extract_inheritors(self) -> List[Dict[str, Any]]:
"""
提取传承人信息
Returns:
List[Dict]: 传承人节点列表
"""
if self.data is None:
raise ValueError("请先读取Excel文件")
self.logger.info("提取传承人信息")
inheritors = []
inheritor_id = 0
for idx, row in self.data.iterrows():
inheritor_names = row.get('代表性传承人', '')
if pd.isna(inheritor_names) or not str(inheritor_names).strip():
continue
# 处理多个传承人(用顿号分隔)
names = str(inheritor_names).replace('', ',').replace('', ',').split(',')
for name in names:
name = name.strip()
if not name:
continue
inheritor_id += 1
inheritor_node = {
"id": f"INH-{inheritor_id:04d}",
"type": "Inheritor",
"properties": {
"inheritor_id": f"INH-{inheritor_id:04d}",
"name": name,
"project_name": str(row.get('项目名称', '')),
"source_row": idx + 2
}
}
inheritors.append(inheritor_node)
self.logger.info(f"提取了 {len(inheritors)} 个传承人")
return inheritors
def extract_relations(self) -> List[Dict[str, Any]]:
"""
提取关系
Returns:
List[Dict]: 关系列表
"""
if self.data is None:
raise ValueError("请先读取Excel文件")
self.logger.info("提取关系")
relations = []
relation_id = 0
for idx, row in self.data.iterrows():
project_id = f"ICH-{idx:04d}"
# 项目-类别关系
category = row.get('类别', '')
if pd.notna(category) and str(category).strip():
relation_id += 1
relations.append({
"id": f"REL-{relation_id:04d}",
"type": "belongs_to",
"from": project_id,
"to": f"CAT-{str(category)}",
"properties": {}
})
# 项目-传承人关系
inheritor_names = row.get('代表性传承人', '')
if pd.notna(inheritor_names) and str(inheritor_names).strip():
names = str(inheritor_names).replace('', ',').replace('', ',').split(',')
for name in names:
name = name.strip()
if name:
relation_id += 1
relations.append({
"id": f"REL-{relation_id:04d}",
"type": "has_inheritor",
"from": project_id,
"to": f"INH-{name}", # 简化处理
"properties": {}
})
self.logger.info(f"提取了 {len(relations)} 个关系")
return relations
def save_analysis_report(self, output_path: str):
"""
保存分析报告
Args:
output_path: 输出路径
"""
analysis = self.analyze_data()
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(analysis, f, indent=2, ensure_ascii=False)
self.logger.info(f"分析报告已保存到: {output_path}")
def save_kg_data(self, nodes: List[Dict], relations: List[Dict], output_dir: str):
"""
保存知识图谱数据
Args:
nodes: 节点列表
relations: 关系列表
output_dir: 输出目录
"""
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# 保存节点
nodes_file = output_path / "nodes.json"
with open(nodes_file, 'w', encoding='utf-8') as f:
json.dump(nodes, f, indent=2, ensure_ascii=False)
self.logger.info(f"节点已保存到: {nodes_file}")
# 保存关系
relations_file = output_path / "relations.json"
with open(relations_file, 'w', encoding='utf-8') as f:
json.dump(relations, f, indent=2, ensure_ascii=False)
self.logger.info(f"关系已保存到: {relations_file}")
def main():
"""主函数"""
# 配置
excel_path = r"E:\Project\2026_KG_ICH\data\黑龙江国家级和省级非遗名单.xlsx"
output_dir = r"E:\Project\2026_KG_ICH\dofile\kg_project\output"
# 创建读取器
reader = ExcelDataReader(excel_path)
# 读取数据
reader.read_excel()
# 分析数据
print("\n" + "="*60)
print("数据分析")
print("="*60)
analysis = reader.analyze_data()
print(f"\n总记录数: {analysis['total_records']}")
print(f"\n类别分布:")
for cat, count in analysis.get('category_distribution', {}).items():
print(f" {cat}: {count}")
print(f"\n传承人覆盖率: {analysis.get('inheritor_coverage', {}).get('coverage_rate', 0):.1f}%")
# 保存分析报告
Path(output_dir).mkdir(parents=True, exist_ok=True)
reader.save_analysis_report(f"{output_dir}/data_analysis.json")
# 清洗数据
print("\n" + "="*60)
print("数据清洗")
print("="*60)
reader.clean_data()
# 转换为知识图谱格式
print("\n" + "="*60)
print("转换为知识图谱格式")
print("="*60)
nodes = reader.convert_to_kg_format()
inheritors = reader.extract_inheritors()
relations = reader.extract_relations()
# 合并所有节点
all_nodes = nodes + inheritors
print(f"\n节点统计:")
print(f" 非遗项目节点: {len(nodes)}")
print(f" 传承人节点: {len(inheritors)}")
print(f" 总节点数: {len(all_nodes)}")
print(f" 关系数: {len(relations)}")
# 保存知识图谱数据
reader.save_kg_data(all_nodes, relations, output_dir)
print("\n" + "="*60)
print("处理完成!")
print("="*60)
if __name__ == '__main__':
main()
+438
View File
@@ -0,0 +1,438 @@
# -*- coding: utf-8 -*-
"""
从Excel提取数据生成知识图谱CSV文件
保持原始数据表述不变
"""
import pandas as pd
import re
import json
from pathlib import Path
def extract_batches(df):
"""提取所有唯一的批次(保持原始表述)"""
batches = {}
batch_counter = {}
for batch_str in df['项目批次'].dropna().unique():
if not batch_str or str(batch_str).strip() == '':
continue
# 解析批次字符串(可能包含多个批次)
batch_items = re.split(r'[,、,]', str(batch_str))
for item in batch_items:
item = item.strip()
if not item:
continue
# 生成批次ID(使用原始表述的哈希)
if item not in batch_counter:
batch_counter[item] = 1
else:
batch_counter[item] += 1
batch_id = f"BATCH-{abs(hash(item)) % 100000:05d}"
if batch_id not in batches:
batches[batch_id] = {
'name': item, # 保持原始表述
'original_string': item
}
return batches
def parse_inheritor_field(inheritor_str):
"""解析传承人字段,保持原始表述(如"吴明新(国)""""
if not inheritor_str or str(inheritor_str).strip() in ['', '']:
return []
# 按顿号、逗号分割,保持原始表述
inheritors = re.split(r'[、,,\n]', str(inheritor_str))
inheritors = [inh.strip() for inh in inheritors if inh.strip() and inh.strip() != '']
return inheritors
def extract_inheritors(df):
"""提取所有唯一传承人(保持原始表述)"""
inheritors = {}
inheritor_counter = {}
for idx, row in df.iterrows():
inheritor_str = row.get('代表性传承人', '')
if not inheritor_str or str(inheritor_str).strip() in ['', '']:
continue
# 解析传承人列表
inheritor_names = parse_inheritor_field(inheritor_str)
for name in inheritor_names:
# 使用原始名称(包括括号)作为key
if name not in inheritor_counter:
inheritor_counter[name] = 1
else:
inheritor_counter[name] += 1
# 生成传承人ID
inheritor_id = f"INH-{abs(hash(name)) % 100000:05d}-{inheritor_counter[name]}"
if inheritor_id not in inheritors:
inheritors[inheritor_id] = {
'name': name # 保持原始表述,如"吴明新(国)"
}
return inheritors
def extract_institutions(df):
"""提取所有唯一保护机构(保持原始表述)"""
institutions = {}
inst_counter = {}
for idx, row in df.iterrows():
inst_str = row.get('项目保护单位', '')
if not inst_str or str(inst_str).strip() in ['', '']:
continue
inst_name = str(inst_str).strip()
# 使用原始机构名
if inst_name not in inst_counter:
inst_counter[inst_name] = 1
else:
inst_counter[inst_name] += 1
# 生成机构ID
inst_id = f"INST-{abs(hash(inst_name)) % 100000:05d}-{inst_counter[inst_name]}"
if inst_id not in institutions:
institutions[inst_id] = {
'name': inst_name # 保持原始表述
}
return institutions
def parse_batch_field(batch_str, batches_dict):
"""解析批次字段,返回批次ID列表"""
if not batch_str or str(batch_str).strip() == '':
return []
batches = re.split(r'[,、,]', str(batch_str))
batch_ids = []
for batch in batches:
batch = batch.strip()
if batch:
# 查找对应的批次ID
for batch_id, batch_info in batches_dict.items():
if batch_info['name'] == batch:
batch_ids.append(batch_id)
break
return batch_ids
def get_institution_id(institution_name, institutions_dict):
"""获取机构ID"""
if not institution_name or str(institution_name).strip() in ['', '']:
return None
# 从已提取的机构中查找
for inst_id, inst_info in institutions_dict.items():
if inst_info['name'] == str(institution_name).strip():
return inst_id
return None
def get_inheritor_id(inheritor_name, inheritors_dict):
"""获取传承人ID"""
if not inheritor_name or not inheritor_name.strip():
return None
# 从已提取的传承人中查找
for inheritor_id, inheritor_info in inheritors_dict.items():
if inheritor_info['name'] == inheritor_name:
return inheritor_id
return None
def clean_data(df):
"""数据清洗"""
# 去除完全空白的行
df = df.dropna(how='all')
# 填充缺失值为空字符串
for col in df.columns:
if df[col].dtype == 'object':
df[col] = df[col].fillna('')
return df
def build_nodes(df):
"""构建节点数据(保持原始表述)"""
nodes_list = []
print("正在构建节点...")
# 1. ICH_Project节点
print(" - 构建ICH_Project节点...")
for idx, row in df.iterrows():
seq_num = row.get('总序号', idx + 1)
project_name = row.get('项目名称', '')
# 确定级别(从批次字段推断)
batch_str = str(row.get('项目批次', ''))
level = '国家级' if '国家级' in batch_str else ('省级' if '省级' in batch_str else '')
nodes_list.append({
'id': f"ICH-{int(seq_num)}",
'label': str(project_name),
'type': 'ICH_Project',
'properties': json.dumps({
'level': level,
'category': row.get('类别', ''), # 保持原始表述,如"曲艺类"
'batch': batch_str,
'protection_unit': row.get('项目保护单位', '')
}, ensure_ascii=False)
})
# 2. Category节点(从数据中动态提取,保持原始表述)
print(" - 构建Category节点...")
unique_categories = df['类别'].dropna().unique()
for cat_name in unique_categories:
# 使用类别名称作为ID(使用哈希避免特殊字符)
cat_id = f"CAT-{abs(hash(cat_name)) % 100000:05d}"
nodes_list.append({
'id': cat_id,
'label': cat_name, # 保持原始表述,如"曲艺类"
'type': 'Category',
'properties': '{}'
})
# 3. Batch节点(动态识别,保持原始表述)
print(" - 构建Batch节点...")
batches = extract_batches(df)
for batch_id, batch_info in batches.items():
nodes_list.append({
'id': batch_id,
'label': batch_info['name'], # 保持原始表述,如"国家级第1批"
'type': 'Batch',
'properties': json.dumps({
'original_string': batch_info['original_string']
}, ensure_ascii=False)
})
# 4. Inheritor节点(动态识别,保持原始表述)
print(" - 构建Inheritor节点...")
inheritors = extract_inheritors(df)
for inheritor_id, inheritor_info in inheritors.items():
nodes_list.append({
'id': inheritor_id,
'label': inheritor_info['name'], # 保持原始表述,如"吴明新(国)"
'type': 'Inheritor',
'properties': '{}'
})
# 5. Institution节点(动态识别,保持原始表述)
print(" - 构建Institution节点...")
institutions = extract_institutions(df)
for inst_id, inst_info in institutions.items():
nodes_list.append({
'id': inst_id,
'label': inst_info['name'], # 保持原始表述
'type': 'Institution',
'properties': '{}'
})
return nodes_list, batches, inheritors, institutions
def build_relations(df, batches, inheritors, institutions):
"""构建关系数据"""
relations_list = []
print("正在构建关系...")
# 建立名称到ID的快速查找映射
category_map = {}
for cat_name in df['类别'].dropna().unique():
cat_id = f"CAT-{abs(hash(cat_name)) % 100000:05d}"
category_map[cat_name] = cat_id
for idx, row in df.iterrows():
seq_num = row.get('总序号', idx + 1)
project_id = f"ICH-{int(seq_num)}"
# 1. 项目 → 类别
category_name = row.get('类别', '')
if category_name and category_name in category_map:
category_id = category_map[category_name]
relations_list.append({
'source': project_id,
'target': category_id,
'type': 'BELONGS_TO',
'properties': '{}'
})
# 2. 项目 → 批次(支持多个批次)
batch_str = row.get('项目批次', '')
if batch_str:
batch_ids = parse_batch_field(batch_str, batches)
for batch_id in batch_ids:
relations_list.append({
'source': project_id,
'target': batch_id,
'type': 'SELECTED_IN_BATCH',
'properties': '{}'
})
# 3. 项目 → 保护机构
institution_name = row.get('项目保护单位', '')
if institution_name and str(institution_name).strip() not in ['', '']:
institution_id = get_institution_id(institution_name, institutions)
if institution_id:
relations_list.append({
'source': project_id,
'target': institution_id,
'type': 'PROTECTED_BY',
'properties': '{}'
})
# 4. 项目 → 传承人(支持多个传承人)
inheritor_str = row.get('代表性传承人', '')
if inheritor_str:
inheritor_names = parse_inheritor_field(inheritor_str)
for name in inheritor_names:
inheritor_id = get_inheritor_id(name, inheritors)
if inheritor_id:
relations_list.append({
'source': project_id,
'target': inheritor_id,
'type': 'HAS_INHERITOR',
'properties': '{}'
})
return relations_list
def generate_report(nodes_df, relations_df, output_dir):
"""生成统计报告"""
report_lines = []
report_lines.append("# 知识图谱CSV提取报告\n")
report_lines.append(f"生成时间: {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
report_lines.append("---\n\n")
# 节点统计
report_lines.append("## 节点统计\n\n")
report_lines.append(f"**节点总数**: {len(nodes_df)}\n\n")
node_type_counts = nodes_df['type'].value_counts().sort_index()
report_lines.append("| 节点类型 | 数量 | 占比 |\n")
report_lines.append("|---------|------|------|\n")
for node_type, count in node_type_counts.items():
percentage = (count / len(nodes_df) * 100)
report_lines.append(f"| {node_type} | {count} | {percentage:.1f}% |\n")
# 关系统计
report_lines.append("\n## 关系统计\n\n")
report_lines.append(f"**关系总数**: {len(relations_df)}\n\n")
rel_type_counts = relations_df['type'].value_counts().sort_index()
report_lines.append("| 关系类型 | 数量 | 占比 |\n")
report_lines.append("|---------|------|------|\n")
for rel_type, count in rel_type_counts.items():
percentage = (count / len(relations_df) * 100)
report_lines.append(f"| {rel_type} | {count} | {percentage:.1f}% |\n")
# 保存报告
report_path = output_dir / 'extraction_report.md'
with open(report_path, 'w', encoding='utf-8') as f:
f.writelines(report_lines)
print(f"\n报告已生成: {report_path}")
# 打印统计信息
print("\n" + "="*50)
print("数据提取完成")
print("="*50)
print(f"\n节点总数: {len(nodes_df)}")
for node_type, count in node_type_counts.items():
print(f" - {node_type}: {count}")
print(f"\n关系总数: {len(relations_df)}")
for rel_type, count in rel_type_counts.items():
print(f" - {rel_type}: {count}")
print("="*50)
def extract_data_from_excel():
"""从Excel提取数据并生成知识图谱CSV文件"""
print("开始从Excel提取数据...")
# 1. 读取Excel
excel_path = r'E:\Project\2026_KG_ICH\data\黑龙江国家级和省级非遗名单.xlsx'
print(f"读取文件: {excel_path}")
df = pd.read_excel(excel_path)
print(f"原始数据: {len(df)} 行 x {len(df.columns)}")
# 2. 提取所需列
columns = ['总序号', '类别', '项目名称', '项目批次', '项目保护单位', '代表性传承人']
print(f"\n提取列: {', '.join(columns)}")
# 检查列是否存在
available_cols = [col for col in columns if col in df.columns]
if len(available_cols) < len(columns):
missing = set(columns) - set(available_cols)
print(f"警告: 以下列不存在: {missing}")
df = df[available_cols]
print(f"提取后数据: {len(df)} 行 x {len(df.columns)}")
# 3. 数据清洗
print("\n数据清洗...")
df = clean_data(df)
print(f"清洗后数据: {len(df)}")
# 4. 构建节点
nodes_list, batches, inheritors, institutions = build_nodes(df)
nodes_df = pd.DataFrame(nodes_list)
print(f"节点数据: {len(nodes_df)}")
# 5. 构建关系
relations_list = build_relations(df, batches, inheritors, institutions)
relations_df = pd.DataFrame(relations_list)
print(f"关系数据: {len(relations_df)}")
# 6. 保存CSV
output_dir = Path(r'E:\Project\2026_KG_ICH\dofile\kg_project\output')
output_dir.mkdir(parents=True, exist_ok=True)
print(f"\n保存文件到: {output_dir}")
# 保存为UTF-8-BOM编码(Excel友好)
nodes_path = output_dir / 'nodes.csv'
relations_path = output_dir / 'rels.csv'
nodes_df.to_csv(nodes_path, index=False, encoding='utf-8-sig')
relations_df.to_csv(relations_path, index=False, encoding='utf-8-sig')
print(f" - nodes.csv: {nodes_path}")
print(f" - rels.csv: {relations_path}")
# 7. 生成统计报告
generate_report(nodes_df, relations_df, output_dir)
return nodes_df, relations_df
if __name__ == '__main__':
extract_data_from_excel()
+382
View File
@@ -0,0 +1,382 @@
# -*- coding: utf-8 -*-
"""
知识图谱可视化工具
使用networkx和matplotlib绘制知识图谱
"""
import pandas as pd
import networkx as nx
import matplotlib.pyplot as plt
from matplotlib import font_manager
import matplotlib.patches as mpatches
from pathlib import Path
import numpy as np
# 设置中文字体
def setup_chinese_font():
"""设置中文字体"""
# 尝试多种中文字体
chinese_fonts = [
'Microsoft YaHei',
'SimHei',
'SimSun',
'KaiTi',
'FangSong',
'STXihei',
'STSong',
'STKaiti',
'STFangsong'
]
for font in chinese_fonts:
try:
plt.rcParams['font.sans-serif'] = [font]
plt.rcParams['axes.unicode_minus'] = False
break
except:
continue
print(f"使用字体: {plt.rcParams['font.sans-serif'][0]}")
# 节点类型颜色映射
NODE_TYPE_COLORS = {
'ICH_Project': '#FF6B6B', # 红色 - 项目
'Category': '#4ECDC4', # 青色 - 类别
'Batch': '#95E1D3', # 绿色 - 批次
'Inheritor': '#FFD93D', # 黄色 - 传承人
'Institution': '#6C5CE7' # 紫色 - 机构
}
# 节点类型大小映射
NODE_TYPE_SIZES = {
'ICH_Project': 300,
'Category': 500,
'Batch': 350,
'Inheritor': 200,
'Institution': 250
}
def load_graph_data(nodes_csv, rels_csv):
"""加载图谱数据"""
print("正在加载数据...")
# 读取节点和关系
nodes_df = pd.read_csv(nodes_csv, encoding='utf-8-sig')
rels_df = pd.read_csv(rels_csv, encoding='utf-8-sig')
print(f" - 节点: {len(nodes_df)}")
print(f" - 关系: {len(rels_df)}")
return nodes_df, rels_df
def build_networkx_graph(nodes_df, rels_df):
"""构建NetworkX图"""
print("正在构建图...")
G = nx.DiGraph() # 有向图
# 添加节点
for idx, row in nodes_df.iterrows():
node_id = row['id']
label = row['label']
node_type = row['type']
# 截断过长的标签
if len(label) > 10:
display_label = label[:10] + '...'
else:
display_label = label
G.add_node(
node_id,
label=display_label,
full_label=label,
node_type=node_type,
color=NODE_TYPE_COLORS.get(node_type, '#CCCCCC'),
size=NODE_TYPE_SIZES.get(node_type, 200)
)
# 添加边
for idx, row in rels_df.iterrows():
source = row['source']
target = row['target']
rel_type = row['type']
if source in G.nodes() and target in G.nodes():
G.add_edge(source, target, rel_type=rel_type)
print(f" - 节点数: {G.number_of_nodes()}")
print(f" - 边数: {G.number_of_edges()}")
return G
def filter_graph_by_type(G, include_types=None):
"""按节点类型过滤图"""
if include_types is None:
return G
nodes_to_keep = [n for n, d in G.nodes(data=True)
if d.get('node_type') in include_types]
return G.subgraph(nodes_to_keep).copy()
def draw_graph(G, output_path, title="知识图谱", layout='spring'):
"""绘制知识图谱"""
print(f"正在绘制图谱: {title}")
plt.figure(figsize=(20, 16))
# 选择布局算法
if layout == 'spring':
pos = nx.spring_layout(G, k=2, iterations=50, seed=42)
elif layout == 'circular':
pos = nx.circular_layout(G)
elif layout == 'kamada_kawai':
pos = nx.kamada_kawai_layout(G)
elif layout == 'random':
pos = nx.random_layout(G)
else:
pos = nx.spring_layout(G, k=2, iterations=50, seed=42)
# 按节点类型分组
node_types = {}
for node, data in G.nodes(data=True):
node_type = data.get('node_type', 'Unknown')
if node_type not in node_types:
node_types[node_type] = []
node_types[node_type].append(node)
# 绘制边
nx.draw_networkx_edges(
G, pos,
alpha=0.3,
width=0.5,
edge_color='gray',
arrows=True,
arrowsize=10,
arrowstyle='->,head_width=0.2,head_length=0.3'
)
# 按类型绘制节点
for node_type, nodes in node_types.items():
color = NODE_TYPE_COLORS.get(node_type, '#CCCCCC')
size = NODE_TYPE_SIZES.get(node_type, 200)
nx.draw_networkx_nodes(
G, pos,
nodelist=nodes,
node_color=color,
node_size=size,
alpha=0.8,
edgecolors='white',
linewidths=1
)
# 绘制标签(只对重要节点)
important_nodes = []
important_labels = {}
for node, data in G.nodes(data=True):
node_type = data.get('node_type')
# 只显示类别、批次和部分重要节点的标签
if node_type in ['Category', 'Batch'] or (
node_type == 'ICH_Project' and data.get('size', 0) > 400
):
important_nodes.append(node)
important_labels[node] = data.get('label', node)
if len(important_nodes) <= 100: # 节点不多时显示所有标签
nx.draw_networkx_labels(
G, pos,
labels=important_labels,
font_size=8,
font_weight='bold',
font_family='sans-serif'
)
else:
# 节点太多时只显示类别标签
category_labels = {n: d['label'] for n, d in G.nodes(data=True)
if d.get('node_type') == 'Category'}
nx.draw_networkx_labels(
G, pos,
labels=category_labels,
font_size=10,
font_weight='bold'
)
# 图例
legend_patches = []
for node_type, color in NODE_TYPE_COLORS.items():
if node_type in node_types:
patch = mpatches.Patch(color=color, label=node_type)
legend_patches.append(patch)
plt.legend(
handles=legend_patches,
loc='upper right',
fontsize=12,
framealpha=0.9
)
plt.title(title, fontsize=16, fontweight='bold', pad=20)
plt.axis('off')
plt.tight_layout()
# 保存图片
plt.savefig(output_path, dpi=150, bbox_inches='tight')
print(f" - 保存到: {output_path}")
plt.close()
def draw_subgraphs(G, output_dir):
"""绘制子图(按节点类型分组)"""
print("\n正在绘制子图...")
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# 1. 只显示项目和类别
print(" 1. 项目-类别关系图...")
G1 = filter_graph_by_type(G, ['ICH_Project', 'Category'])
draw_graph(
G1,
output_dir / 'kg_project_category.png',
title='非遗项目与类别关系',
layout='spring'
)
# 2. 只显示项目和传承人
print(" 2. 项目-传承人关系图...")
G2 = filter_graph_by_type(G, ['ICH_Project', 'Inheritor'])
if G2.number_of_nodes() > 0:
draw_graph(
G2,
output_dir / 'kg_project_inheritor.png',
title='非遗项目与传承人关系',
layout='spring'
)
# 3. 只显示项目、类别和批次
print(" 3. 项目-类别-批次关系图...")
G3 = filter_graph_by_type(G, ['ICH_Project', 'Category', 'Batch'])
draw_graph(
G3,
output_dir / 'kg_project_category_batch.png',
title='非遗项目、类别与批次关系',
layout='kamada_kawai'
)
# 4. 完整图谱(抽样显示)
print(" 4. 完整知识图谱...")
if G.number_of_nodes() > 500:
# 节点太多时,只显示连接度高的节点
degrees = dict(G.degree())
high_degree_nodes = [n for n, d in degrees.items() if d >= 3]
G_sample = G.subgraph(high_degree_nodes).copy()
draw_graph(
G_sample,
output_dir / 'kg_full_sampled.png',
title=f'完整知识图谱(抽样,显示{G_sample.number_of_nodes()}个节点)',
layout='spring'
)
else:
draw_graph(
G,
output_dir / 'kg_full.png',
title='完整知识图谱',
layout='spring'
)
def print_statistics(G):
"""打印图统计信息"""
print("\n" + "="*60)
print("图谱统计信息")
print("="*60)
print(f"\n节点总数: {G.number_of_nodes()}")
print(f"边总数: {G.number_of_edges()}")
# 按类型统计节点
print("\n节点类型分布:")
node_types = {}
for node, data in G.nodes(data=True):
node_type = data.get('node_type', 'Unknown')
node_types[node_type] = node_types.get(node_type, 0) + 1
for node_type, count in sorted(node_types.items()):
percentage = (count / G.number_of_nodes() * 100)
print(f" - {node_type}: {count} ({percentage:.1f}%)")
# 按类型统计边
print("\n关系类型分布:")
rel_types = {}
for u, v, data in G.edges(data=True):
rel_type = data.get('rel_type', 'Unknown')
rel_types[rel_type] = rel_types.get(rel_type, 0) + 1
for rel_type, count in sorted(rel_types.items()):
percentage = (count / G.number_of_edges() * 100)
print(f" - {rel_type}: {count} ({percentage:.1f}%)")
# 连接度统计
degrees = [d for n, d in G.degree()]
print(f"\n连接度统计:")
print(f" - 平均连接度: {np.mean(degrees):.2f}")
print(f" - 最大连接度: {max(degrees)}")
print(f" - 最小连接度: {min(degrees)}")
# 找出连接度最高的节点
top_nodes = sorted(G.degree(), key=lambda x: x[1], reverse=True)[:10]
print(f"\n连接度最高的10个节点:")
for node, degree in top_nodes:
node_data = G.nodes[node]
label = node_data.get('full_label', node)
node_type = node_data.get('node_type', '')
print(f" - [{node_type}] {label}: {degree}个连接")
print("="*60)
def visualize_kg(nodes_csv, rels_csv, output_dir):
"""可视化知识图谱"""
print("="*60)
print("知识图谱可视化工具")
print("="*60)
# 设置中文字体
setup_chinese_font()
# 加载数据
nodes_df, rels_df = load_graph_data(nodes_csv, rels_csv)
# 构建图
G = build_networkx_graph(nodes_df, rels_df)
# 打印统计信息
print_statistics(G)
# 绘制子图
draw_subgraphs(G, output_dir)
print("\n" + "="*60)
print("可视化完成!")
print("="*60)
if __name__ == '__main__':
# 输入文件
nodes_csv = r'E:\Project\2026_KG_ICH\dofile\kg_project\output\nodes.csv'
rels_csv = r'E:\Project\2026_KG_ICH\dofile\kg_project\output\rels.csv'
# 输出目录
output_dir = r'E:\Project\2026_KG_ICH\dofile\kg_project\output\visualizations'
# 执行可视化
visualize_kg(nodes_csv, rels_csv, output_dir)