Files
pengxiao 219232de74 refactor: 重组项目目录结构
以讲义内容为骨架迁移到标准目录格式:
- officefile/ 主内容(12章 + 附录 + CC4SI补充)
- dofile/ 代码示例(11个Python脚本)
- data/ 图片资源
- output/ 生成输出(忽略)
- Archive/ 归档旧目录(忽略)
- .claude/skills/ 保留markdown-to-docx工具链
- .pandoc/ 保留CSL和本地化配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:00:56 +08:00

963 lines
30 KiB
Python

"""
空间分析助手类 (Spatial Analysis Assistant)
========================================
这是一个完整的空间分析助手实现,展示了如何构建一个基础的
空间智能系统。该系统可以帮助用户进行空间数据处理、分析和可视化。
主要功能:
1. 空间数据加载与管理
2. 空间关系计算 (距离、方位、包含关系等)
3. 空间统计分析
4. 空间插值与预测
5. 多准则决策分析
作者: CC4SI 项目组
日期: 2025-01
"""
import json
import math
from typing import List, Dict, Tuple, Optional, Any, Union
from dataclasses import dataclass, field
from enum import Enum
import random
# ============================================================================
# 数据结构定义
# ============================================================================
class GeometryType(Enum):
"""几何类型枚举"""
POINT = "Point"
LINESTRING = "LineString"
POLYGON = "Polygon"
MULTIPOINT = "MultiPoint"
@dataclass
class Point:
"""点几何类"""
x: float
y: float
z: Optional[float] = None
properties: Dict[str, Any] = field(default_factory=dict)
def __repr__(self) -> str:
if self.z is not None:
return f"Point({self.x:.2f}, {self.y:.2f}, {self.z:.2f})"
return f"Point({self.x:.2f}, {self.y:.2f})"
@dataclass
class BoundingBox:
"""边界框类"""
min_x: float
min_y: float
max_x: float
max_y: float
@property
def width(self) -> float:
"""获取边界框宽度"""
return self.max_x - self.min_x
@property
def height(self) -> float:
"""获取边界框高度"""
return self.max_y - self.min_y
@property
def center(self) -> Point:
"""获取边界框中心点"""
return Point(
(self.min_x + self.max_x) / 2,
(self.min_y + self.max_y) / 2
)
def contains(self, point: Point) -> bool:
"""检查点是否在边界框内"""
return (self.min_x <= point.x <= self.max_x and
self.min_y <= point.y <= self.max_y)
def __repr__(self) -> str:
return f"BoundingBox[{self.min_x:.2f},{self.min_y:.2f} -> {self.max_x:.2f},{self.max_y:.2f}]"
@dataclass
class SpatialFeature:
"""空间要素类"""
id: str
geometry: Union[Point, List[Point]] # 简化: 点或点列表
properties: Dict[str, Any] = field(default_factory=dict)
def __repr__(self) -> str:
return f"SpatialFeature(id={self.id}, geometry={type(self.geometry).__name__})"
# ============================================================================
# 空间分析助手主类
# ============================================================================
class SpatialHelper:
"""
空间分析助手类
这是系统的核心类,提供空间数据处理和分析的主要功能。
"""
def __init__(self, name: str = "空间分析助手", version: str = "1.0.0"):
"""
初始化空间分析助手
Args:
name: 助手名称
version: 版本号
"""
self.name = name
self.version = version
self.features: List[SpatialFeature] = []
self.coordinate_system: str = "EPSG:4326" # 默认 WGS84
self.metadata: Dict[str, Any] = {}
print(f"[{self.name}] v{self.version} 初始化完成")
# ------------------------------------------------------------------------
# 数据加载与管理
# ------------------------------------------------------------------------
def add_feature(self, feature: SpatialFeature) -> bool:
"""
添加空间要素
Args:
feature: 要添加的空间要素
Returns:
是否添加成功
"""
try:
self.features.append(feature)
return True
except Exception as e:
print(f"添加要素失败: {e}")
return False
def add_point(self, x: float, y: float, feature_id: str = None,
properties: Dict[str, Any] = None) -> SpatialFeature:
"""
添加点要素
Args:
x: X坐标
y: Y坐标
feature_id: 要素ID
properties: 属性字典
Returns:
创建的空间要素
"""
if feature_id is None:
feature_id = f"point_{len(self.features)}"
point = Point(x, y)
feature = SpatialFeature(
id=feature_id,
geometry=point,
properties=properties or {}
)
self.add_feature(feature)
return feature
def load_from_geojson(self, geojson_str: str) -> int:
"""
从 GeoJSON 字符串加载数据
Args:
geojson_str: GeoJSON 格式字符串
Returns:
加载的要素数量
"""
try:
data = json.loads(geojson_str)
count = 0
if data.get("type") == "FeatureCollection":
for feature_data in data.get("features", []):
feature = self._parse_geojson_feature(feature_data)
if feature:
self.add_feature(feature)
count += 1
print(f"从 GeoJSON 加载了 {count} 个要素")
return count
except json.JSONDecodeError as e:
print(f"GeoJSON 解析失败: {e}")
return 0
def _parse_geojson_feature(self, feature_data: Dict) -> Optional[SpatialFeature]:
"""解析 GeoJSON 要素"""
try:
feature_id = feature_data.get("id", f"feature_{len(self.features)}")
properties = feature_data.get("properties", {})
geometry = feature_data.get("geometry", {})
geom_type = geometry.get("type")
if geom_type == "Point":
coordinates = geometry.get("coordinates", [])
point = Point(coordinates[0], coordinates[1])
return SpatialFeature(id=feature_id, geometry=point, properties=properties)
return None
except Exception:
return None
def get_feature_by_id(self, feature_id: str) -> Optional[SpatialFeature]:
"""根据ID获取要素"""
for feature in self.features:
if feature.id == feature_id:
return feature
return None
def get_feature_count(self) -> int:
"""获取要素数量"""
return len(self.features)
def clear_features(self) -> None:
"""清空所有要素"""
self.features.clear()
print("已清空所有要素")
# ------------------------------------------------------------------------
# 空间关系计算
# ------------------------------------------------------------------------
@staticmethod
def calculate_distance(point1: Point, point2: Point) -> float:
"""
计算两点间的欧氏距离
Args:
point1: 第一个点
point2: 第二个点
Returns:
距离值
"""
dx = point2.x - point1.x
dy = point2.y - point1.y
return math.sqrt(dx * dx + dy * dy)
@staticmethod
def calculate_bearing(point1: Point, point2: Point) -> float:
"""
计算从 point1 到 point2 的方位角 (度)
Args:
point1: 起始点
point2: 目标点
Returns:
方位角 (0-360度)
"""
dx = point2.x - point1.x
dy = point2.y - point1.y
radians = math.atan2(dy, dx)
degrees = math.degrees(radians)
return (degrees + 360) % 360
@staticmethod
def calculate_midpoint(point1: Point, point2: Point) -> Point:
"""
计算两点间的中点
Args:
point1: 第一个点
point2: 第二个点
Returns:
中点
"""
return Point(
(point1.x + point2.x) / 2,
(point1.y + point2.y) / 2
)
def find_nearest_neighbor(self, target_point: Point,
max_distance: float = float('inf')) -> Optional[Tuple[SpatialFeature, float]]:
"""
查找最近邻要素
Args:
target_point: 目标点
max_distance: 最大搜索距离
Returns:
(最近的要素, 距离) 或 None
"""
nearest_feature = None
min_distance = float('inf')
for feature in self.features:
if isinstance(feature.geometry, Point):
dist = self.calculate_distance(target_point, feature.geometry)
if dist < min_distance and dist <= max_distance:
min_distance = dist
nearest_feature = feature
if nearest_feature:
return nearest_feature, min_distance
return None
def find_neighbors_within_distance(self, target_point: Point,
distance: float) -> List[Tuple[SpatialFeature, float]]:
"""
查找指定距离内的所有要素
Args:
target_point: 目标点
distance: 搜索半径
Returns:
(要素, 距离) 列表,按距离排序
"""
neighbors = []
for feature in self.features:
if isinstance(feature.geometry, Point):
dist = self.calculate_distance(target_point, feature.geometry)
if dist <= distance:
neighbors.append((feature, dist))
neighbors.sort(key=lambda x: x[1])
return neighbors
def calculate_bounding_box(self) -> Optional[BoundingBox]:
"""
计算所有要素的边界框
Returns:
边界框对象,如果没有要素则返回 None
"""
if not self.features:
return None
points = [f.geometry for f in self.features if isinstance(f.geometry, Point)]
if not points:
return None
min_x = min(p.x for p in points)
max_x = max(p.x for p in points)
min_y = min(p.y for p in points)
max_y = max(p.y for p in points)
return BoundingBox(min_x, min_y, max_x, max_y)
# ------------------------------------------------------------------------
# 空间统计分析
# ------------------------------------------------------------------------
def calculate_centroid(self) -> Optional[Point]:
"""
计算所有点要素的质心
Returns:
质心点
"""
points = [f.geometry for f in self.features if isinstance(f.geometry, Point)]
if not points:
return None
avg_x = sum(p.x for p in points) / len(points)
avg_y = sum(p.y for p in points) / len(points)
return Point(avg_x, avg_y)
def calculate_mean_center(self, weight_field: str = None) -> Optional[Point]:
"""
计算加权或未加权的平均中心
Args:
weight_field: 权重字段名
Returns:
平均中心点
"""
points = []
weights = []
for feature in self.features:
if isinstance(feature.geometry, Point):
points.append(feature.geometry)
if weight_field:
weights.append(feature.properties.get(weight_field, 1.0))
else:
weights.append(1.0)
if not points:
return None
total_weight = sum(weights)
avg_x = sum(p.x * w for p, w in zip(points, weights)) / total_weight
avg_y = sum(p.y * w for p, w in zip(points, weights)) / total_weight
return Point(avg_x, avg_y)
def calculate_standard_distance(self) -> Optional[float]:
"""
计算标准距离 (标准差圆)
Returns:
标准距离值
"""
centroid = self.calculate_centroid()
if not centroid:
return None
points = [f.geometry for f in self.features if isinstance(f.geometry, Point)]
if not points:
return None
n = len(points)
squared_distances = [(p.x - centroid.x)**2 + (p.y - centroid.y)**2 for p in points]
return math.sqrt(sum(squared_distances) / n)
def calculate_spatial_autocorrelation(self, field: str) -> Optional[float]:
"""
计算 Moran's I 空间自相关指数
Args:
field: 要分析的属性字段
Returns:
Moran's I 值
"""
# 简化实现: 使用距离权重
points_data = []
for feature in self.features:
if isinstance(feature.geometry, Point) and field in feature.properties:
points_data.append((feature.geometry, feature.properties[field]))
n = len(points_data)
if n < 2:
return None
mean_value = sum(v for _, v in points_data) / n
# 计算权重矩阵 (距离倒数)
weights = {}
total_weight = 0
for i, (p1, v1) in enumerate(points_data):
for j, (p2, v2) in enumerate(points_data):
if i != j:
dist = self.calculate_distance(p1, p2)
w = 1 / (dist + 0.001) # 避免除零
weights[(i, j)] = w
total_weight += w
# 计算 Moran's I
numerator = 0
denominator = 0
for i, (p1, v1) in enumerate(points_data):
for j, (p2, v2) in enumerate(points_data):
if i != j:
w = weights.get((i, j), 0)
numerator += w * (v1 - mean_value) * (v2 - mean_value)
denominator += (v1 - mean_value) ** 2
if denominator == 0:
return None
morans_i = (n / total_weight) * (numerator / denominator)
return morans_i
# ------------------------------------------------------------------------
# 空间插值与预测
# ------------------------------------------------------------------------
def inverse_distance_weighting(self, target_point: Point, power: float = 2.0,
field: str = "value", max_distance: float = None) -> Optional[float]:
"""
反距离加权插值 (IDW)
Args:
target_point: 目标点
power: 距离幂次
field: 插值字段
max_distance: 最大搜索距离
Returns:
插值结果
"""
points_data = []
for feature in self.features:
if isinstance(feature.geometry, Point) and field in feature.properties:
points_data.append((feature.geometry, feature.properties[field]))
if not points_data:
return None
numerator = 0.0
denominator = 0.0
for point, value in points_data:
dist = self.calculate_distance(target_point, point)
if max_distance and dist > max_distance:
continue
if dist < 1e-10: # 几乎重合
return value
weight = 1.0 / (dist ** power)
numerator += weight * value
denominator += weight
if denominator == 0:
return None
return numerator / denominator
def simple_trend_prediction(self, field: str, target_x: float,
target_y: float) -> Optional[float]:
"""
基于简单趋势的预测 (线性回归)
Args:
field: 预测字段
target_x: 目标X坐标
target_y: 目标Y坐标
Returns:
预测值
"""
points_data = []
for feature in self.features:
if isinstance(feature.geometry, Point) and field in feature.properties:
points_data.append({
'x': feature.geometry.x,
'y': feature.geometry.y,
'z': feature.properties[field]
})
if len(points_data) < 3:
return None
# 简单的线性趋势: z = a + b*x + c*y
n = len(points_data)
sum_x = sum(p['x'] for p in points_data)
sum_y = sum(p['y'] for p in points_data)
sum_z = sum(p['z'] for p in points_data)
sum_xx = sum(p['x']**2 for p in points_data)
sum_yy = sum(p['y']**2 for p in points_data)
sum_xy = sum(p['x'] * p['y'] for p in points_data)
sum_xz = sum(p['x'] * p['z'] for p in points_data)
sum_yz = sum(p['y'] * p['z'] for p in points_data)
# 简化: 只使用x方向趋势
try:
# z = a + b*x
b = (n * sum_xz - sum_x * sum_z) / (n * sum_xx - sum_x**2)
a = (sum_z - b * sum_x) / n
return a + b * target_x
except ZeroDivisionError:
return None
# ------------------------------------------------------------------------
# 多准则决策分析 (MCDA)
# ------------------------------------------------------------------------
def weighted_sum_model(self, criteria: List[str], weights: List[float],
feature_ids: List[str] = None) -> List[Tuple[SpatialFeature, float]]:
"""
加权求和模型 (WSM)
Args:
criteria: 评价准则列表
weights: 各准则权重
feature_ids: 参与评价的要素ID列表
Returns:
(要素, 得分) 列表,按得分降序排列
"""
if len(criteria) != len(weights):
print("错误: 准则数量与权重数量不匹配")
return []
if abs(sum(weights) - 1.0) > 0.001:
print(f"警告: 权重总和为 {sum(weights)}, 建议归一化为 1.0")
# 确定评价范围
features_to_eval = self.features
if feature_ids:
features_to_eval = [f for f in self.features if f.id in feature_ids]
results = []
# 归一化参数
min_max = {}
for criterion in criteria:
values = []
for f in features_to_eval:
if criterion in f.properties:
values.append(f.properties[criterion])
if values:
min_max[criterion] = (min(values), max(values))
for feature in features_to_eval:
score = 0.0
valid = True
for criterion, weight in zip(criteria, weights):
if criterion not in feature.properties:
valid = False
break
value = feature.properties[criterion]
cmin, cmax = min_max.get(criterion, (0, 1))
# 归一化 (假设值越大越好)
if cmax - cmin > 0:
normalized = (value - cmin) / (cmax - cmin)
else:
normalized = 0.5
score += weight * normalized
if valid:
results.append((feature, score))
results.sort(key=lambda x: x[1], reverse=True)
return results
def weighted_product_model(self, criteria: List[str], weights: List[float],
feature_ids: List[str] = None) -> List[Tuple[SpatialFeature, float]]:
"""
加权乘积模型 (WPM)
Args:
criteria: 评价准则列表
weights: 各准则权重
feature_ids: 参与评价的要素ID列表
Returns:
(要素, 得分) 列表,按得分降序排列
"""
if len(criteria) != len(weights):
print("错误: 准则数量与权重数量不匹配")
return []
features_to_eval = self.features
if feature_ids:
features_to_eval = [f for f in self.features if f.id in feature_ids]
results = []
# 归一化参数
min_max = {}
for criterion in criteria:
values = []
for f in features_to_eval:
if criterion in f.properties:
values.append(f.properties[criterion])
if values:
min_max[criterion] = (min(values), max(values))
for feature in features_to_eval:
product = 1.0
valid = True
for criterion, weight in zip(criteria, weights):
if criterion not in feature.properties:
valid = False
break
value = feature.properties[criterion]
cmin, cmax = min_max.get(criterion, (0, 1))
if cmax - cmin > 0:
normalized = (value - cmin) / (cmax - cmin)
else:
normalized = 1.0
product *= normalized ** weight
if valid:
results.append((feature, product))
results.sort(key=lambda x: x[1], reverse=True)
return results
def topsis(self, criteria: List[str], weights: List[float],
benefit_criteria: List[bool] = None) -> List[Tuple[SpatialFeature, float]]:
"""
TOPSIS (逼近理想解排序法)
Args:
criteria: 评价准则列表
weights: 各准则权重
benefit_criteria: 是否为效益型准则 (True=越大越好, False=越小越好)
Returns:
(要素, 相对贴近度) 列表,按贴近度降序排列
"""
if benefit_criteria is None:
benefit_criteria = [True] * len(criteria)
features_to_eval = [f for f in self.features
if all(c in f.properties for c in criteria)]
if len(features_to_eval) == 0:
return []
n = len(features_to_eval)
m = len(criteria)
# 构建决策矩阵
decision_matrix = []
for feature in features_to_eval:
row = [feature.properties[c] for c in criteria]
decision_matrix.append(row)
# 归一化决策矩阵
normalized_matrix = []
for j in range(m):
column = [decision_matrix[i][j] for i in range(n)]
norm = math.sqrt(sum(x**2 for x in column))
for i in range(n):
if j == 0:
normalized_matrix.append([])
normalized_matrix[i].append(decision_matrix[i][j] / norm if norm > 0 else 0)
# 加权归一化矩阵
weighted_matrix = []
for i in range(n):
weighted_matrix.append([normalized_matrix[i][j] * weights[j] for j in range(m)])
# 确定理想解和负理想解
ideal_positive = []
ideal_negative = []
for j in range(m):
column = [weighted_matrix[i][j] for i in range(n)]
if benefit_criteria[j]:
ideal_positive.append(max(column))
ideal_negative.append(min(column))
else:
ideal_positive.append(min(column))
ideal_negative.append(max(column))
# 计算距离和相对贴近度
results = []
for i, feature in enumerate(features_to_eval):
dist_positive = math.sqrt(
sum((weighted_matrix[i][j] - ideal_positive[j])**2 for j in range(m))
)
dist_negative = math.sqrt(
sum((weighted_matrix[i][j] - ideal_negative[j])**2 for j in range(m))
)
closeness = dist_negative / (dist_positive + dist_negative) if (dist_positive + dist_negative) > 0 else 0
results.append((feature, closeness))
results.sort(key=lambda x: x[1], reverse=True)
return results
# ------------------------------------------------------------------------
# 缓冲区分析
# ------------------------------------------------------------------------
def create_buffer_analysis(self, feature_id: str, buffer_distance: float) -> Dict[str, Any]:
"""
缓冲区分析
Args:
feature_id: 中心要素ID
buffer_distance: 缓冲距离
Returns:
分析结果字典
"""
target_feature = self.get_feature_by_id(feature_id)
if not target_feature or not isinstance(target_feature.geometry, Point):
return {"error": "找不到指定的点要素"}
center = target_feature.geometry
# 查找缓冲区内的要素
features_in_buffer = self.find_neighbors_within_distance(center, buffer_distance)
# 计算统计信息
values_in_buffer = [f.properties for f, _ in features_in_buffer]
return {
"center_feature": feature_id,
"buffer_distance": buffer_distance,
"count": len(features_in_buffer),
"features": [(f.id, dist) for f, dist in features_in_buffer],
"statistics": {
"avg_distance": sum(dist for _, dist in features_in_buffer) / len(features_in_buffer) if features_in_buffer else 0
}
}
# ------------------------------------------------------------------------
# 可视化辅助 (文本形式)
# ------------------------------------------------------------------------
def print_summary(self) -> None:
"""打印数据摘要"""
print(f"\n{'='*50}")
print(f"空间分析助手摘要: {self.name} v{self.version}")
print(f"{'='*50}")
print(f"要素数量: {len(self.features)}")
print(f"坐标系: {self.coordinate_system}")
bbox = self.calculate_bounding_box()
if bbox:
print(f"边界范围: {bbox}")
centroid = self.calculate_centroid()
if centroid:
print(f"质心位置: {centroid}")
print(f"{'='*50}\n")
def print_features(self, limit: int = 10) -> None:
"""打印要素列表"""
print(f"\n要素列表 (显示前 {min(limit, len(self.features))} 个):")
print("-" * 60)
for i, feature in enumerate(self.features[:limit]):
if isinstance(feature.geometry, Point):
print(f"{i+1}. ID: {feature.id:15s} 位置: {feature.geometry} 属性: {feature.properties}")
if len(self.features) > limit:
print(f"... 还有 {len(self.features) - limit} 个要素")
print("-" * 60 + "\n")
# ============================================================================
# 辅助函数
# ============================================================================
def create_sample_data(helper: SpatialHelper, n_points: int = 20) -> None:
"""创建示例数据"""
print(f"生成 {n_points} 个随机样本点...")
random.seed(42) # 可重现的随机数
# 生成随机点
for i in range(n_points):
x = random.uniform(0, 100)
y = random.uniform(0, 100)
value = random.uniform(0, 100)
population = random.randint(100, 10000)
accessibility = random.uniform(0.3, 0.95)
helper.add_point(
x=x,
y=y,
feature_id=f"point_{i:03d}",
properties={
"value": value,
"population": population,
"accessibility": accessibility,
"name": f"位置_{i+1}"
}
)
print(f"已生成 {helper.get_feature_count()} 个样本点")
# ============================================================================
# 主程序
# ============================================================================
def main():
"""主程序 - 演示空间分析助手的使用"""
print("="*70)
print("空间分析助手 - 完整示例演示")
print("="*70)
# 1. 创建助手实例
print("\n[步骤 1] 创建空间分析助手")
helper = SpatialHelper(name="城市空间分析助手", version="1.0.0")
# 2. 添加示例数据
print("\n[步骤 2] 添加示例数据")
create_sample_data(helper, n_points=15)
# 3. 打印数据摘要
print("\n[步骤 3] 数据摘要")
helper.print_summary()
helper.print_features()
# 4. 空间关系计算
print("\n[步骤 4] 空间关系计算")
print("-" * 50)
test_point = Point(50, 50)
print(f"测试点: {test_point}")
nearest = helper.find_nearest_neighbor(test_point)
if nearest:
feature, dist = nearest
print(f"最近邻: {feature.id}, 距离: {dist:.2f}")
neighbors = helper.find_neighbors_within_distance(test_point, 25)
print(f"半径25内的邻居数量: {len(neighbors)}")
# 5. 空间统计分析
print("\n[步骤 5] 空间统计分析")
print("-" * 50)
centroid = helper.calculate_centroid()
print(f"质心: {centroid}")
std_dist = helper.calculate_standard_distance()
print(f"标准距离: {std_dist:.2f}")
# 6. 多准则决策分析
print("\n[步骤 6] 多准则决策分析 (TOPSIS)")
print("-" * 50)
criteria = ["accessibility", "population"]
weights = [0.6, 0.4] # 可达性权重更高
results = helper.topsis(criteria, weights)
print("选址优先级排序 (基于可达性和人口):")
for i, (feature, score) in enumerate(results[:5]):
print(f" {i+1}. {feature.properties.get('name', feature.id)}: 得分={score:.4f}")
# 7. 空间插值
print("\n[步骤 7] 空间插值预测")
print("-" * 50)
predict_point = Point(45, 55)
predicted = helper.inverse_distance_weighting(predict_point, field="value")
print(f"{predict_point} 处的插值预测: {predicted:.2f}")
# 8. 缓冲区分析
print("\n[步骤 8] 缓冲区分析")
print("-" * 50)
buffer_result = helper.create_buffer_analysis("point_000", 30)
print(f"以 point_000 为中心,半径30的缓冲区:")
print(f" 包含要素数: {buffer_result['count']}")
print(f" 平均距离: {buffer_result['statistics']['avg_distance']:.2f}")
print("\n" + "="*70)
print("演示完成!")
print("="*70)
if __name__ == "__main__":
main()