周五上午,机加车间质量追溯室。
"这批轴又超差了,"质量工程师阿凯把三坐标报告推过来,"同一把刀、同一程序、同一批毛坯,连续 50 件,前 10 件直径 Φ25.002,到第 50 件变成 Φ25.038,公差带是 ±0.02,后 20 件全废了。客户退了 8 箱。"
我点开机加 MES 导出表。
"这里面有什么?"我问。
"每批次一条汇总 + 每件一条实测,"阿凯说,"主轴转速、进给速度、切深、刀补值、冷却液浓度、环境温湿度、刀具累计加工时长、装夹扭矩、对刀基准偏移、G 代码版本,全记着。但尺寸漂移是'慢慢跑出去的',系统只做单件 SPC 报警,等 Xbar 图出红点,已经跑了 15 件了。"
"我就想干一件事,"阿凯说,"把过去三个月 120 个批次灌进去,自动找出'到底是哪几个工艺因素在推着尺寸往正方向漂'。别跟我说'综合影响',我要能跟工艺组说'主轴温升+刀具磨损+刀补未补偿,这三个解释 82% 的漂移方差,冷却液浓度和装夹扭矩可以不用管'。"
"尺寸漂移不是突变,是累积偏移,"我接话,"像你每天往同一个方向挪桌子 1 毫米,半个月就偏出半米。用 pandas 做批次聚合+特征构造(单件偏差斜率、刀具磨损量、温升积分),numpy 算滑动回归斜率,scikit-learn 的 GradientBoostingRegressor 建模漂移量 + SHAP 风格排列重要性,scipy 做趋势显著性检验(线性回归 p 值 + 相关系数),matplotlib 画漂移曲线+因素热力图+重要性条形+相关网络+残差图+温漂叠加图,networkx 建'工艺因素→尺寸漂移'因果链路。"
"对,"阿凯点头,"要能说清'为什么是温升不是冷却液'。我拿去跟评审说'主轴连续转 4 小时后热伸长 12μm,刀补没跟,尺寸正漂',专家就认这个。"
"用 pandas 做批次特征工程,sklearn GBR 回归+Permutation,scipy 趋势检验,matplotlib 出 6 图,networkx 画因果链,存 results/,"我开工程,"数据自包含,合成 120 批次 × 每批 50 件,下载就能跑。"
敲了行原型:
# 目标: 从多工艺因素中定位驱动尺寸漂移的主因
# 方法: 批次级漂移斜率回归 + GBR + Permutation + 趋势显著性
# 输出: 主因排序 + 漂移量预测 + 补偿建议
"完整版 OOP 封好,"我说,"数据加载器、批次特征工程器、漂移分析器、因素定位器、可视化器、决策链路,输出主因排名+6图+报告。"
阿凯凑近看:"那以后看报告:批次 B073,50 件直径从 +2μm 漂到 +38μm,线性斜率 +0.76μm/件(p<0.001)。主因:主轴热伸长贡献 41%、刀具累计磨损 27%、刀补未补偿 14%,三者合计 82%;冷却液浓度贡献 3%(p=0.41 不显著)。建议:每加工 20 件执行一次热机后刀补补偿,或增加主轴温控回路。"
"对,"我接话,"数控加工不是'测出来超差再返工',是'算出来往哪漂、谁推的、提前补'。数字孪生里挂尺寸漂移敏感度节点,这套就是工艺员的'尺寸罗盘'——不用等 Xbar 图红了才知道跑偏。"
一、实际应用场景(真实痛点)
场景设定:数控加工中,同程序同刀具连续加工批次,尺寸常呈现单向缓慢漂移(多为热伸长导致的正漂或刀具磨损导致的负漂)。传统 SPC 单件控制图对"渐变型偏移"响应滞后,等报警时已成批超差。工艺因素多达 10+ 项,且存在耦合(如主轴温升与运行时长强相关、刀补与磨损强相关),人工归因极难。
现场原话(叙事化):
"不是我们没做质控,"阿凯说,"Xbar-R 图天天在跑,但它是'看单件跳不跳'。尺寸是像爬楼梯一样慢慢上去的,前 30 件都在控制限内,第 31 件刚出限,后面 19 件已经全废了。SPC 救不了渐变漂移。"
"最烦的是归因,"阿凯补充,"工艺组每次开会都吵:有人说冷却液稀了,有人说装夹松了,有人说毛坯硬了。三个说法都有道理,但没人能量化'到底谁推了 80% 的漂移量'。三个月 120 个批次,数据全在 MES 里睡大觉,就等有人把它算明白。"
核心矛盾:"单件 SPC + 经验归因 + 数据沉睡" 与 "批次级漂移建模 + 因素贡献量化 + 趋势显著性检验 + 主动刀补补偿" 之间的断层。
二、痛点分析(映射到滨州职业学院《先进制造技术》课程模型)
《先进制造技术》模块 本篇痛点对应
数控加工与CAD/CAM技术:工艺参数优化与精度控制 尺寸漂移归因 + 刀补策略
先进制造技术基础:公差配合与精度理论 渐变型尺寸偏移 vs 统计公差
智能制造与数字孪生:工艺敏感度建模 热-力耦合漂移因素定位
柔性制造系统FMS与先进生产管理:质量追溯 批次级质量数据回溯
一句话总结:我们需要一个"数控批次数据→单件漂移斜率提取+GBR回归+Permutation重要性+趋势显著性检验+主因定位程序",用
"pandas" 做批次聚合/特征构造,
"numpy" 算滑动回归斜率,
"scikit-learn" GBR+Permutation,
"scipy" 线性回归显著性+相关系数,
"matplotlib" 画漂移曲线+因素热力图+重要性+相关网络+残差+温漂叠加,
"networkx" 建因果链路,实现从"超差再返工"到"数据驱动的主因定位+提前补偿"。
三、核心逻辑讲解(大白话)
3.1 问题本质:把尺寸漂移想成"每天往右挪 1 毫米的桌子"
把一批零件想成你每天在桌上画一条线:
* 单件实测偏差 = 每天画的那条线位置
* 漂移斜率 = 每天往右挪了几毫米(正漂=变大,负漂=变小)
* 主轴温升 = 桌子被晒热了会膨胀
* 刀具磨损 = 笔尖变钝画出来偏左
* 刀补未补偿 = 你知道偏了但没改程序
* 你的目标 = 算出"每天往右挪的那几毫米,主要是晒热贡献的,还是笔钝贡献的"
3.2 业务逻辑 → 代码映射
导入数控批次数据(批次+单件实测)
│
▼ BatchDataLoader (pandas)
读取表:
批次号, 件号, 实测直径, 标称直径,
主轴转速, 进给, 切深, 刀补值,
主轴温度, 刀具累计时长, 冷却液浓度,
环境温湿度, 装夹扭矩, 对刀偏移
│
▼ BatchFeatureEngineer (pandas + numpy)
批次级特征构造:
单件偏差 = 实测 - 标称
漂移斜率 = np.polyfit(件号, 偏差, 1)[0] # μm/件
温升积分 = ∑(主轴温度-室温)×Δt
刀具磨损量 = f(累计时长)
刀补缺口 = 理论补偿量 - 实际刀补值
热伸长估算 = α×L×ΔT
│
▼ DriftAnalyzer (scipy)
趋势显著性:
对每批做线性回归: 偏差~件号
输出斜率 + p值 + R²
标记"显著正漂/显著负漂/稳定"
│
▼ FactorLocator (scikit-learn)
因素定位:
GBR 回归: 用工艺因素预测"漂移斜率"
Permutation Importance → 因素贡献排序
偏相关 → 拆开耦合变量(温升 vs 运行时长)
综合得分 = 0.5×GBR + 0.3×Perm + 0.2×|标准化回归系数|
│
▼ NCVisualizer (matplotlib + networkx)
可视化:
1. 典型批次漂移曲线(实测点+拟合线+公差带)
2. 因素-漂移相关性热力图
3. 因素贡献重要性条形图
4. 主因因果网络(因素→漂移)
5. 模型残差图(验证拟合质量)
6. 温升+磨损+刀补缺口叠加贡献图
│
▼ SyntheticNCBatch (numpy)
合成数据:
120批次 × 每批50件
主因: 主轴温升>刀具磨损>刀补缺口
可复现
3.3 为什么不能"单件 SPC"
视角 问题
Xbar 控制图 对渐变漂移滞后 15-30 件
单因素对比 温升与运行时长共线,分不清
批次斜率回归 直接量化"每件的漂移速度"
因素贡献分解 GBR+Permutation+偏相关三重定位
主动补偿 算出来就补,不等超差
3.4 分析前后对比
维度 传统方式 本程序
漂移发现 超差后报警 斜率显著即预警
归因方式 开会拍脑袋 贡献度量化(p值)
补偿动作 整批返工 每 N 件补刀补
置信度 "大概是温升" "温升贡献41%, p<0.001"
四、OOP 代码实现
4.1 项目结构
nc_drift_mining/
├── nc_drift_mining/
│ ├── __init__.py
│ ├── batch_data_loader.py # 数据加载
│ ├── batch_feature_engineer.py # 批次特征工程
│ ├── drift_analyzer.py # 漂移趋势检验(scipy)
│ ├── factor_locator.py # 因素定位(sklearn)
│ ├── nc_visualizer.py # 可视化
│ └── synthetic_nc_batch.py # 合成数据
├── tests/
│ ├── __init__.py
│ └── test_nc_drift.py
├── results/
│ ├── drift_curve.png
│ ├── factor_heatmap.png
│ ├── factor_importance.png
│ ├── causality_network.png
│ ├── residual_plot.png
│ ├── contribution_stack.png
│ ├── factor_detail.csv
│ └── drift_report.txt
└── run_nc_drift.py
4.2 核心源码
<details>
<summary></summary>
"""数控批次数据加载器"""
import pandas as pd
from pathlib import Path
from typing import Optional
class BatchDataLoader:
"""读取数控加工批次数据(批次+单件实测)"""
def __init__(self, filepath: str = "nc_batch_data.csv",
encoding: str = "utf-8"):
self.filepath = Path(filepath)
self.encoding = encoding
def load(self) -> pd.DataFrame:
if not self.filepath.exists():
raise FileNotFoundError(self.filepath)
df = pd.read_csv(self.filepath, encoding=self.encoding)
req = ["batch_id", "piece_seq", "measured_dia_mm", "nominal_dia_mm"]
miss = [c for c in req if c not in df.columns]
if miss:
raise ValueError(f"缺列: {miss}")
num_cols = ["measured_dia_mm", "nominal_dia_mm", "piece_seq"]
for c in num_cols:
df[c] = pd.to_numeric(df[c], errors="coerce")
df = df.dropna(subset=["measured_dia_mm"]).reset_index(drop=True)
return df
def summary(self, df: pd.DataFrame) -> str:
s = f"批次数量: {df['batch_id'].nunique()}\n"
s += f"单件总数: {len(df)}\n"
s += f"标称直径: {df['nominal_dia_mm'].iloc[0]:.3f} mm\n"
dev = (df["measured_dia_mm"] - df["nominal_dia_mm"]) * 1000
s += f"偏差范围: {dev.min():.1f}μm ~ {dev.max():.1f}μm"
return s
</details>
<details>
<summary></summary>
"""批次级特征工程 (pandas + numpy)"""
import numpy as np
import pandas as pd
from typing import List
class BatchFeatureEngineer:
"""把单件数据聚合成批次级漂移特征"""
def __init__(self, alpha: float = 11.5e-6,
tool_len_mm: float = 120.0,
room_temp: float = 20.0):
self.alpha = alpha # 主轴钢热膨胀系数 1/℃
self.tool_len = tool_len_mm
self.room_temp = room_temp
def add_deviation(self, df: pd.DataFrame) -> pd.DataFrame:
result = df.copy()
result["dev_um"] = (result["measured_dia_mm"] -
result["nominal_dia_mm"]) * 1000.0
return result
def aggregate_batch(self, df: pd.DataFrame) -> pd.DataFrame:
"""每个批次算一条特征"""
df = self.add_deviation(df)
rows = []
for batch_id, g in df.groupby("batch_id"):
g = g.sort_values("piece_seq")
x = g["piece_seq"].values.astype(float)
y = g["dev_um"].values.astype(float)
# 漂移斜率 μm/件
if len(g) >= 3:
slope, intercept = np.polyfit(x, y, 1)
y_fit = slope * x + intercept
ss_res = np.sum((y - y_fit) ** 2)
ss_tot = np.sum((y - y.mean()) ** 2)
r2 = 1 - ss_res / ss_tot if ss_tot > 1e-9 else 0.0
else:
slope, r2 = 0.0, 0.0
# 取批次级工艺参数(取均值或首值)
row = {
"batch_id": batch_id,
"n_pieces": len(g),
"drift_slope_um_per_pc": float(slope),
"drift_r2": float(r2),
"start_dev_um": float(y[0]),
"end_dev_um": float(y[-1]),
"spindle_speed_rpm": g["spindle_speed_rpm"].mean(),
"feed_mm_min": g["feed_mm_min"].mean(),
"depth_of_cut_mm": g["depth_of_cut_mm"].mean(),
"tool_offset_um": g["tool_offset_um"].mean(),
"spindle_temp_c": g["spindle_temp_c"].mean(),
"tool_runtime_h": g["tool_runtime_h"].mean(),
"coolant_pct": g["coolant_pct"].mean(),
"ambient_temp_c": g["ambient_temp_c"].mean(),
"clamp_torque_nm": g["clamp_torque_nm"].mean(),
"align_offset_um": g["align_offset_um"].mean(),
}
# 温升积分
dT = row["spindle_temp_c"] - self.room_temp
row["temp_rise_integral"] = float(dT * row["n_pieces"])
# 热伸长估算 (直径方向, 对称放大2倍)
row["thermal_growth_um"] = float(
2 * self.alpha * self.tool_len * 1e3 * dT)
# 刀具磨损估算(线性近似)
row["tool_wear_um"] = float(0.8 * row["tool_runtime_h"])
# 刀补缺口
row["offset_gap_um"] = float(
row["thermal_growth_um"] + row["tool_wear_um"]
- row["tool_offset_um"])
rows.append(row)
return pd.DataFrame(rows)
def get_factor_columns(self) -> List[str]:
return [
"spindle_temp_c", "tool_runtime_h", "tool_offset_um",
"temp_rise_integral", "thermal_growth_um", "tool_wear_um",
"offset_gap_um", "feed_mm_min", "depth_of_cut_mm",
"coolant_pct", "ambient_temp_c", "clamp_torque_nm",
"align_offset_um", "spindle_speed_rpm",
]
</details>
<details>
<summary></summary>
"""漂移趋势显著性检验 (scipy)"""
import numpy as np
import pandas as pd
from scipy import stats
from typing import Dict
class DriftAnalyzer:
"""对每批做线性回归显著性检验"""
def __init__(self, alpha: float = 0.05):
self.alpha = alpha
def analyze_batch(self, df: pd.DataFrame) -> pd.DataFrame:
"""输入单件级数据, 输出每批趋势判定"""
df = df.copy()
if "dev_um" not in df.columns:
df["dev_um"] = (df["measured_dia_mm"] -
df["nominal_dia_mm"]) * 1000.0
results = []
for batch_id, g in df.groupby("batch_id"):
g = g.sort_values("piece_seq")
x = g["piece_seq"].values.astype(float)
y = g["dev_um"].values.astype(float)
if len(g) < 3:
results.append({"batch_id": batch_id, "slope": 0,
"p_value": 1.0, "r2": 0,
"trend": "insufficient"})
continue
slope, intercept = np.polyfit(x, y, 1)
# scipy 线性回归显著性
res = stats.linregress(x, y)
p_val = res.pvalue
r2 = res.rvalue ** 2
if p_val < self.alpha:
if slope > 0:
trend = "significant_positive_drift"
else:
trend = "significant_negative_drift"
else:
trend = "stable"
results.append({
"batch_id": batch_id,
"slope": float(slope),
"p_value": float(p_val),
"r2": float(r2),
"trend": trend,
})
return pd.DataFrame(results)
def summary_stats(self, batch_df: pd.DataFrame) -> Dict:
n_pos = (batch_df["trend"] == "significant_positive_drift").sum()
n_neg = (batch_df["trend"] == "significant_negative_drift").sum()
n_stable = (batch_df["trend"] == "stable").sum()
return {
"positive_drift_batches": int(n_pos),
"negative_drift_batches": int(n_neg),
"stable_batches": int(n_stable),
"mean_slope": float(batch_df["slope"].mean()),
"max_abs_slope": float(batch_df["slope"].abs().max()),
}
</details>
<details>
<summary></summary>
"""因素定位 (scikit-learn + scipy)"""
import numpy as np
from typing import Dict, List
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.inspection import permutation_importance
from sklearn.model_selection import cross_val_score
from scipy import stats
class FactorLocator:
"""用工艺因素预测漂移斜率, 定位主因"""
def __init__(self, random_state: int = 42):
self.random_state = random_state
self.model_ = None
def fit(self, X: np.ndarray, y: np.ndarray) -> GradientBoostingRegressor:
self.model_ = GradientBoostingRegressor(
n_estimators=200,
learning_rate=0.05,
max_depth=4,
min_samples_leaf=3,
random_state=self.random_state,
)
self.model_.fit(X, y)
return self.model_
def gbr_importance(self, feature_names: List[str]) -> Dict:
imp = dict(zip(feature_names,
self.model_.feature_importances_))
return dict(sorted(imp.items(), key=lambda x: x[1], reverse=True))
def permutation_score(self, X: np.ndarray, y: np.ndarray,
feature_names: List[str]) -> Dict:
perm = permutation_importance(self.model_, X, y,
n_repeats=20,
random_state=self.random_state,
scoring="r2")
result = {}
for i, name in enumerate(feature_names):
result[name] = {
"mean": float(perm.importances_mean[i]),
"std": float(perm.importances_std[i]),
}
return dict(sorted(result.items(),
key=lambda x: x[1]["mean"], reverse=True))
def pearson_with_target(self, X: np.ndarray, y: np.ndarray,
feature_names: List[str]) -> Dict:
"""单变量与漂移斜率的相关性 + p值"""
result = {}
for i, name in enumerate(feature_names):
r, p = stats.pearsonr(X[:, i], y)
result[name] = {"r": float(r), "p_value": float(p)}
return result
def composite_score(self, gbr_imp: Dict, perm_imp: Dict,
corr: Dict, feature_names: List[str]) -> Dict:
"""综合得分 = 0.5×GBR + 0.3×Perm(归一化) + 0.2×|r|"""
# 归一化
gbr_sum = sum(gbr_imp.values()) + 1e-9
gbr_norm = {k: v / gbr_sum for k, v in gbr_imp.items()}
perm_vals = [max(v["mean"], 0) for v in perm_imp.values()]
perm_sum = sum(perm_vals) + 1e-9
perm_norm = {k: max(perm_imp[k]["mean"], 0) / perm_sum
for k in perm_imp}
composite = {}
for feat in feature_names:
r = abs(corr.get(feat, {}).get("r", 0))
score = (0.5 * gbr_norm.get(feat, 0) +
0.3 * perm_norm.get(feat, 0) +
0.2 * r)
p_val = corr.get(feat, {}).get("p_value", 1.0)
composite[feat] = {
"score": float(score),
"gbr_norm": float(gbr_norm.get(feat, 0)),
"perm_norm": float(perm_norm.get(feat, 0)),
"abs_r": float(r),
"p_value": float(p_val),
"significant": p_val < 0.05,
}
return dict(sorted(composite.items(),
key=lambda x: x[1]["score"], reverse=True))
def cross_validate(self, X: np.ndarray, y: np.ndarray,
cv: int = 5) -> Dict:
scores = cross_val_score(self.model_, X, y,
cv=cv, scoring="r2")
return {"r2_mean": float(scores.mean()),
"r2_std": float(scores.std())}
def predict_drift(self, X: np.ndarray) -> np.ndarray:
return self.model_.predict(X)
def recommend_compensation(self, composite: Dict,
top_n: int = 3) -> List[str]:
return [k for k, _ in
list(composite.items())[:top_n]]
</details>
<details>
<summary></summary>
"""可视化 (matplotlib + networkx)"""
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
import networkx as nx
from typing import Dict, List
plt.rcParams["font.sans-serif"] = ["SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
class NCVisualizer:
def __init__(self, results_dir: str = "results"):
self.results_dir = Path(results_dir)
self.results_dir.mkdir(exist_ok=True)
def drift_curve(self, df_pieces: pd.DataFrame, batch_id: str,
tol_um: float = 20.0):
"""典型批次漂移曲线 + 公差带"""
sub = df_pieces[df_pieces["batch_id"] == batch_id].sort_values("piece_seq")
if len(sub) == 0:
sub = df_pieces.head(50)
dev = (sub["measured_dia_mm"] - sub["nominal_dia_mm"]) * 1000
fig, ax = plt.subplots(figsize=(11, 6))
ax.scatter(sub["piece_seq"], dev, c="#3498DB", s=35,
edgecolors="black", label="单件实测偏差", zorder=3)
# 拟合线
x = sub["piece_seq"].values.astype(float)
y = dev.values
slope, intercept = np.polyfit(x, y, 1)
ax.plot(x, slope * x + intercept, "r--", linewidth=2.5,
label=f"拟合斜率 {slope:.3f}μm/件")
ax.axhline(tol_um, color="orange", ls="--", lw=1.8,
label=f"+公差 {tol_um}μm")
ax.axhline(-tol_um, color="orange", ls="--", lw=1.8,
label=f"-公差 {tol_um}μm")
ax.set_xlabel("件号(同批次顺序)", fontsize=12)
ax.set_ylabel("直径偏差 (μm)", fontsize=12)
ax.set_title(f"批次 {batch_id} 尺寸漂移曲线",
fontsize=13, fontweight="bold")
ax.legend(fontsize=10)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(self.results_dir/"drift_curve.png",
dpi=150, bbox_inches="tight")
plt.close()
def factor_heatmap(self, batch_df: pd.DataFrame,
factor_cols: List[str]):
"""因素-漂移相关性热力图"""
cols = factor_cols + ["drift_slope_um_per_pc"]
corr = batch_df[cols].corr()
fig, ax = plt.subplots(figsize=(12, 10))
im = ax.imshow(corr.values, cmap="RdBu_r", vmin=-1, vmax=1,
aspect="auto")
ax.set_xticks(range(len(cols)))
ax.set_yticks(range(len(cols)))
ax.set_xticklabels(cols, rotation=45, ha="right", fontsize=8)
ax.set_yticklabels(cols, fontsize=8)
for i in range(len(cols)):
for j in range(len(cols)):
v = corr.values[i, j]
c = "white" if abs(v) > 0.6 else "black"
ax.text(j, i, f"{v:.2f}", ha="center", va="center",
color=c, fontsize=7)
plt.colorbar(im, ax=ax, label="Pearson r")
ax.set_title("工艺因素-漂移斜率相关性热力图",
fontsize=13, fontweight="bold")
plt.tight_layout()
plt.savefig(self.results_dir/"factor_heatmap.png",
dpi=150, bbox_inches="tight")
plt.close()
def factor_importance(self, composite: Dict):
"""因素贡献重要性"""
fig, ax = plt.subplots(figsize=(11, 6))
names = list(composite.keys())[:8]
vals = [composite[n]["score"] for n in names]
sig = [composite[n]["significant"] for n in names]
colors = ["#E74C3C" if s else "#95A5A6" for s in sig[::-1]]
ax.barh(range(len(names)), vals[::-1], color=colors,
edgecolor="black", height=0.6)
ax.set_yticks(range(len(names)))
ax.set_yticklabels(names[::-1], fontsize=10)
ax.set_xlabel("综合贡献得分")
ax.set_title("尺寸漂移主因贡献排序(红=显著 p<0.05)",
fontsize=13, fontweight="bold")
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
plt.savefig(self.results_dir/"factor_importance.png",
dpi=150, bbox_inches="tight")
plt.close()
def causality_network(self, composite: Dict, top_n: int = 5):
"""主因因果网络"""
fig, ax = plt.subplots(figsize=(12, 8))
G = nx.DiGraph()
G.add_node("尺寸漂移", ntype="target")
top = list(composite.items())[:top_n]
for feat, info in top:
G.add_node(feat, ntype="factor")
G.add_edge(feat, "尺寸漂移",
weight=round(info["score"], 3))
pos = nx.spring_layout(G, seed=42, k=2.2)
node_colors = ["#E74C3C" if G.nodes[n]["ntype"] == "target"
else "#3498DB" for n in G.nodes()]
nx.draw_networkx_nodes(G, pos, node_color=node_colors,
node_size=3000, alpha=0.85, ax=ax)
edges = [(u, v) for u, v in G.edges()]
weights = [G[u][v]["weight"] * 8 for u, v in edges]
nx.draw_networkx_edges(G, pos, wid
利用AI解决实际问题,如果你觉得这个工具好用,欢迎关注长安牧笛!