python的先进制造技术工业场景模拟第三十四篇:加载增材制造不同打印方向试样数据,对比不同摆放方向零件尺寸偏差。 周二上午SLM 金属打印车间。这批钛合金试样同一个模型分别按 平放XY面贴基板、竖放Z向立起、45°斜放 三种方向打印质检员老马把三份三坐标报告摊在桌上图纸要求关键尺寸公差 ±0.1mm结果竖放的那批Z向高度偏差到了 0.18mm平放的XY方向反而稳在 ±0.05mm 内。客户问为什么同个模型同台设备方向一变就超差。我点开测量数据 CSV。这表里有什么老马问。每条是某个试样某个特征点的实测值带理论值、实测值、打印方向、试样编号我指着屏幕问题是它只给点数据没按方向聚合偏差也没做统计对比。现在靠肉眼看三坐标报告平放看一页、竖放看一页比不出规律。我就想干一件事老马说把不同打印方向的试样按方向分组算每个尺寸特征长/宽/高/孔径的偏差分布对比哪几个方向在哪些轴上超差画出来再做个显著性检验确认竖放Z向偏大是不是统计意义上的规律而不是偶然。比如平放试样XY方向偏差均值 0.02mmZ向 0.06mm我接话因为层厚堆叠在Z向有累积效应。竖放时高度变成Z向堆叠层厚误差被放大均值就到 0.18mm 了。45°斜放是各向折中但斜向支撑痕迹会让表面尺寸波动变大标准差反而高。对老马点头还有我想看不同方向下哪些特征最容易超差能不能聚类出高风险方向-特征组合以后出工艺卡直接标注此件禁止竖放做高度关键面。用 pandas 按方向特征聚合偏差numpy 算均值/标准差/CPKmatplotlib 画分组箱线图偏差热力图方向对比柱状图scipy 做多方向 ANOVA 和两两 t 检验scikit-learn 聚类方向×特征风险画像networkx 建方向-特征偏差关系网我开工程数据自包含合成一批 SLM 试样三坐标数据下载就能跑。敲了行原型dev df.groupby([orient,feature])[deviation].agg([mean,std])cpk (0.1 - np.abs(dev)) / (3*std)完整版用 OOP 封好我说一个类管数据加载一个类算偏差统计一个类算过程能力一个类做显著性检验一个类做风险聚类一个类建关系网一个类出图。输出方向对比表、CPK表、超差清单、5张图存 results/。老马凑近看那以后看报告平放 Z向均值0.06mm CPK 1.82竖放 Z向均值0.18mm CPK 0.71已1不可接受45°斜放各向标准差最大。ANOVA 显示 Z向偏差在三方向间 p0.0001显著。聚类把竖放×高度标为高风险红区。工艺卡直接加一条高度公差严的件默认平放Z向堆叠方向避开关键尺寸。对我接话增材不是想怎么摆都行摆放方向就是精度设计的一部分。数字孪生里建打印变形模型这些方向-特征偏差数据就是标定基准。一、实际应用场景真实痛点场景设定SLM 金属增材车间同模型多方向摆放打印平放/竖放/斜放三坐标测量关键特征尺寸。工艺与质检需要量化不同打印方向下的尺寸偏差规律识别超差方向-特征组合输出可落地的摆放工艺规范。现场原话叙事化不是我们设备不稳老马说同一台设备、同一批粉、同个模型平放全合格竖放就高度超差。以前我们以为是偶发返工了几次才发现是方向问题。三坐标报告一页一页翻没人把三个方向放一起比过。还有层厚的事老马补充Z向是层层堆出来的每层的铺粉误差、热收缩都往Z向叠。平放时Z向只是厚度不重要竖放时高度就是Z向立刻放大。这种各向异性不按方向拆开统计永远当随机误差处理。核心矛盾三坐标点云测量流水 与 按打印方向聚合偏差 过程能力评价 方向间显著性检验 方向×特征风险聚类 工艺规范输出 之间的断层。二、痛点分析映射到滨州职业学院《先进制造技术》课程模型《先进制造技术》模块 本篇痛点对应增材制造(3D打印)技术SLM工艺、各向异性、摆放策略 不同打印方向尺寸偏差对比先进制造技术基础公差与测量技术、过程能力 CPK评价 偏差分布数控加工与CAD/CAM技术尺寸精度控制思想迁移 关键特征公差带管理智能制造与数字孪生打印变形预测模型 方向-特征偏差作为孪生标定数据先进制造新模式数据驱动工艺规范 聚类输出摆放禁忌规则一句话总结我们需要一个增材制造多打印方向尺寸偏差对比程序用pandas 按方向特征聚合numpy 算偏差统计与 CPKmatplotlib 画箱线图/热力图/对比柱图scipy 做 ANOVA 与 t 检验scikit-learn 聚类方向×特征风险画像networkx 建方向-特征偏差关系网实现从点云测量值到方向级偏差规律 过程能力 超差定位 工艺规范。三、核心逻辑讲解大白话3.1 问题本质把打印方向想成叠积木的方向把 SLM 打印想成用薄纸片叠一个方块* 平放 纸片平着叠方块的高度方向就是叠纸方向Z长宽在桌面上XY* 竖放 把方块立起来原来叠纸的方向变成了高度叠出来的累积误差全显在关键尺寸上* 45°斜放 斜着叠各方向都沾点层纹表面波动大* 理论值 图纸尺寸* 实测值 三坐标打出来的数* 偏差 实测 - 理论正就是胀负就是缩* CPK 看这个过程稳不稳得住公差* 方向对比 同一个特征三个方向摆一起看谁飘得厉害* 风险聚类 自动圈出哪个方向哪个尺寸最危险3.2 业务逻辑 → 代码映射导入三坐标测量数据│▼ OrientLoader (pandas)读取 CSVsample_id, orient(平放/竖放/斜放), feature(长/宽/高/孔径),nominal, measured算 deviation measured - nominal│▼ DeviationStats (numpy/pandas)按方向×特征聚合mean/dev/std/abs_mean│▼ CapabilityCalculator (numpy)过程能力Cp 公差带 / (6σ)Cpk min(USL-μ, μ-LSL) / (3σ)│▼ SignificanceTester (scipy)方向间对比同特征下三方向 ANOVA竖放 vs 平放 两两 t 检验│▼ RiskProfiler (scikit-learn)方向×特征聚类特征[|均值偏差|, 标准差, Cpk]KMeans → 安全区/警戒区/高风险区│▼ OrientGraph (networkx)方向-特征关系网节点方向/特征边权绝对偏差大小│▼ OrientVisualizer (matplotlib)可视化1. 分组箱线图(各方向各特征偏差分布)2. 方向×特征偏差热力图3. 各方向CPK对比柱状图4. 风险聚类散点图5. 方向-特征关系网│▼ SyntheticSpecimenGenerator (numpy)合成数据平放/竖放/斜放 × 长/宽/高/孔径含竖放Z向系统性正偏(层厚累积)3.3 为什么不能只看平均偏差视角 问题全方向平均偏差 0.05mm 掩盖竖放高度 0.18mm只看均值 忽略斜放标准差大表面波动方向×特征拆分 CPK 直接判定能否接单ANOVA检验 确认是方向导致不是随机3.4 分析前后对比维度 三坐标报告翻页 本程序方向对比 人工肉眼比 分组统计ANOVA过程能力 不计算 自动算CPK超差定位 逐点看 方向×特征矩阵风险规则 老师傅经验 聚类出可复用规则工艺卡输出 无 自动标注摆放禁忌四、OOP 代码实现4.1 项目结构am_orient_analysis/├── am_orient_analysis/│ ├── __init__.py│ ├── orient_loader.py # 数据加载│ ├── deviation_stats.py # 偏差统计│ ├── capability_calculator.py # CPK计算│ ├── significance_tester.py # 统计检验(scipy)│ ├── risk_profiler.py # 风险聚类(sklearn)│ ├── orient_graph.py # 方向-特征关系网(networkx)│ ├── visualizer.py # 可视化│ └── synthetic_data.py # 合成数据├── tests/│ ├── __init__.py│ └── test_orient_analysis.py├── results/│ ├── boxplot_deviation.png│ ├── deviation_heatmap.png│ ├── cpk_bar.png│ ├── risk_cluster.png│ ├── orient_network.png│ ├── orient_dev_summary.csv│ ├── cpk_table.csv│ ├── risk_clusters.csv│ └── orient_report.txt└── run_orient_analysis.py4.2 核心源码detailssummary/summary增材制造试样测量数据加载器import pandas as pdfrom pathlib import Pathfrom typing import OptionalVALID_ORIENTS [平放, 竖放, 斜放]class OrientLoader:加载三坐标测量CSVdef __init__(self, filepath: str specimen_measure.csv,encoding: str utf-8):self.filepath Path(filepath)self.encoding encodingself._raw: Optional[pd.DataFrame] Nonedef load(self) - pd.DataFrame:if not self.filepath.exists():raise FileNotFoundError(f文件不存在: {self.filepath})self._raw pd.read_csv(self.filepath, encodingself.encoding)rename {}for tgt, al in {sample_id: [sample_id, 试样编号, sample],orient: [orient, 打印方向, direction],feature: [feature, 特征, dim],nominal: [nominal, 理论值, nom],measured: [measured, 实测值, act],}.items():if tgt not in self._raw.columns:for a in al:if a in self._raw.columns:rename[a] tgtbreakself._raw self._raw.rename(columnsrename)req [orient, feature, nominal, measured]miss [c for c in req if c not in self._raw.columns]if miss:raise ValueError(f缺少必要列: {miss})self._raw[orient] self._raw[orient].astype(str).str.strip()omap {}for o in self._raw[orient].unique():for vo in VALID_ORIENTS:if vo in o or o in vo:omap[o] vobreakif o not in omap:omap[o] 平放self._raw[orient] self._raw[orient].map(omap).fillna(平放)self._raw[feature] self._raw[feature].astype(str).str.strip()self._raw[sample_id] self._raw.get(sample_id, S001).astype(str).str.strip()self._raw[nominal] pd.to_numeric(self._raw[nominal], errorscoerce)self._raw[measured] pd.to_numeric(self._raw[measured], errorscoerce)self._raw self._raw.dropna(subset[nominal, measured]).copy()self._raw[deviation] (self._raw[measured] - self._raw[nominal]).round(4)return self._raw.reset_index(dropTrue)/detailsdetailssummary/summary偏差统计 (numpy/pandas)import numpy as npimport pandas as pdfrom typing import Optionalclass DeviationStats:按方向×特征聚合偏差def __init__(self):passdef summarize(self, df: pd.DataFrame) - pd.DataFrame:rows []for (orient, feat), g in df.groupby([orient, feature]):dev g[deviation].valuesrows.append({orient: orient,feature: feat,n: len(dev),mean_dev: round(float(np.mean(dev)), 4),abs_mean_dev: round(float(np.mean(np.abs(dev))), 4),std_dev: round(float(np.std(dev, ddof1)), 4) if len(dev) 1 else 0.0,max_abs_dev: round(float(np.max(np.abs(dev))), 4),min_dev: round(float(np.min(dev)), 4),max_dev: round(float(np.max(dev)), 4),})out pd.DataFrame(rows)return out.sort_values([feature, orient]).reset_index(dropTrue)def over_tolerance(self, summary_df: pd.DataFrame,tol: float 0.1) - pd.DataFrame:标记超差项out summary_df.copy()out[over_tol] out[max_abs_dev] tolreturn out/detailsdetailssummary/summary过程能力计算 CPK (numpy)import numpy as npimport pandas as pdfrom typing import Optionalclass CapabilityCalculator:Cp (USL - LSL) / (6σ)Cpk min(USL-μ, μ-LSL) / (3σ)默认对称公差 ±toldef __init__(self, tol: float 0.1):self.tol toldef compute(self, summary_df: pd.DataFrame) - pd.DataFrame:out summary_df.copy()usl self.tollsl -self.tolmu out[mean_dev]sigma out[std_dev].replace(0, 1e-6)cp (usl - lsl) / (6 * sigma)cpu (usl - mu) / (3 * sigma)cpl (mu - lsl) / (3 * sigma)cpk np.minimum(cpu, cpl)out[Cp] cp.round(3)out[Cpk] cpk.round(3)out[capability_level] out[Cpk].apply(self._level)return outstaticmethoddef _level(cpk: float) - str:if cpk 1.33:return A级(能力充足)if cpk 1.0:return B级(勉强可接受)if cpk 0.67:return C级(需管控)return D级(不可接受)/detailsdetailssummary/summary方向间显著性检验 (scipy)import numpy as npimport pandas as pdfrom scipy import statsfrom typing import Dict, Listclass SignificanceTester:同特征下多方向 ANOVA 两两 t 检验def __init__(self, alpha: float 0.05):self.alpha alphadef anova_by_feature(self, df: pd.DataFrame) - pd.DataFrame:rows []for feat, g in df.groupby(feature):groups [v[deviation].values for _, v in g.groupby(orient)]groups [x for x in groups if len(x) 1]if len(groups) 2:continuef_stat, p_val stats.f_oneway(*groups)rows.append({feature: feat,f_statistic: round(float(f_stat), 4),p_value: round(float(p_val), 4),significant: bool(p_val self.alpha),})return pd.DataFrame(rows).sort_values(p_value).reset_index(dropTrue)def pairwise_ttest(self, df: pd.DataFrame,feature: str,a: str, b: str) - Dict:da df[(df[feature] feature) (df[orient] a)][deviation].valuesdb df[(df[feature] feature) (df[orient] b)][deviation].valuesif len(da) 2 or len(db) 2:return {feature: feature, a: a, b: b,p_value: np.nan, significant: False}stat, p stats.ttest_ind(da, db, equal_varFalse)return {feature: feature, a: a, b: b,mean_a: round(float(np.mean(da)), 4),mean_b: round(float(np.mean(db)), 4),p_value: round(float(p), 4),significant: bool(p self.alpha),}def full_pairwise(self, df: pd.DataFrame) - pd.DataFrame:out []orients [平放, 竖放, 斜放]for feat in df[feature].unique():for i in range(len(orients)):for j in range(i1, len(orients)):out.append(self.pairwise_ttest(df, feat, orients[i], orients[j]))return pd.DataFrame(out)/detailsdetailssummary/summary方向×特征风险聚类 (scikit-learn)import numpy as npimport pandas as pdfrom sklearn.cluster import KMeansfrom sklearn.preprocessing import StandardScalerfrom typing import Optionalclass RiskProfiler:基于偏差幅度/波动/CPK聚类风险等级def __init__(self, n_clusters: int 3, random_state: int 42):self.n_clusters n_clustersself.random_state random_statedef profile(self, cap_df: pd.DataFrame) - pd.DataFrame:df cap_df.copy()feat_cols [abs_mean_dev, std_dev, Cpk]for c in feat_cols:if c not in df.columns:df[c] 0.0X df[feat_cols].fillna(0).values# Cpk越大越好, 反向处理X[:, 2] -X[:, 2]scaler StandardScaler()Xs scaler.fit_transform(X)km KMeans(n_clustersself.n_clusters, random_stateself.random_state)df[risk_cluster] km.fit_predict(Xs)centers scaler.inverse_transform(km.cluster_centers_)label_map {}for i, c in enumerate(centers):abs_mean, std, neg_cpk ccpk -neg_cpkif cpk 1.0:label_map[i] 高风险区elif cpk 1.33:label_map[i] 警戒区else:label_map[i] 安全区df[risk_level] df[risk_cluster].map(label_map)return dfdef risk_summary(self, df: pd.DataFrame) - pd.DataFrame:if risk_level not in df.columns:return pd.DataFrame()rows []for lvl, g in df.groupby(risk_level):rows.append({risk_level: lvl,count: len(g),items: , .join((g[orient] × g[feature]).tolist()[:10]),min_cpk: round(g[Cpk].min(), 3),max_abs_dev: round(g[abs_mean_dev].max(), 4),})order {高风险区:0, 警戒区:1, 安全区:2}return pd.DataFrame(rows).sort_values(risk_level, keylambda s: s.map(order)).reset_index(dropTrue)/detailsdetailssummary/summary方向-特征偏差关系网 (networkx)import networkx as nximport pandas as pdfrom typing import Optionalclass OrientGraph:建方向-特征关系网, 边权绝对偏差def __init__(self):self.G nx.DiGraph()def build(self, cap_df: pd.DataFrame) - nx.DiGraph:self.G.clear()for o in cap_df[orient].unique():self.G.add_node(fO:{o}, ntypeorient)for f in cap_df[feature].unique():self.G.add_node(fF:{f}, ntypefeature)for _, r in cap_df.iterrows():o_node fO:{r[orient]}f_node fF:{r[feature]}w float(r[abs_mean_dev]) * 100self.G.add_edge(o_node, f_node,weightround(w, 2),cpkr[Cpk])# 超差标记if r[Cpk] 1.0:self.G.nodes[f_node][risk] highreturn self.Gdef high_risk_edges(self) - pd.DataFrame:rows []for u, v, d in self.G.edges(dataTrue):if d.get(cpk, 9) 1.0:rows.append({from: u.split(:)[-1],to: v.split(:)[-1],cpk: d[cpk],weight: d[weight],})return pd.DataFrame(rows).sort_values(cpk).reset_index(dropTrue)/detailsdetailssummary/summary可视化 (matplotlib)import numpy as npimport pandas as pdimport matplotlib.pyplot as pltfrom pathlib import Pathplt.rcParams[font.sans-serif] [SimHei, DejaVu Sans]plt.rcParams[axes.unicode_minus] Falseclass OrientVisualizer:def __init__(self, results_dir: str results):self.results_dir Path(results_dir)self.results_dir.mkdir(exist_okTrue)def boxplot(self, df, tol0.1):各方向各特征偏差箱线图features df[feature].unique()orients [平放, 竖放, 斜放]fig, ax plt.subplots(figsize(13, 6))data []labels []colors {平放:#27AE60,竖放:#E74C3C,斜放:#F39C12}for i, feat in enumerate(features):for o in orients:vals df[(df[feature]feat) (df[orient]o)][deviation].valuesif len(vals):data.append(vals)labels.append(f{feat}\n{o})bp ax.boxplot(data, patch_artistTrue, labelslabels, showmeansTrue)idx 0for i, feat in enumerate(features):for o in orients:bp[boxes][idx].set_facecolor(colors[o])bp[boxes][idx].set_alpha(0.7)idx 1ax.axhline(tol, colorgray, ls--, lw1, labelf公差{tol})ax.axhline(-tol, colorgray, ls--, lw1)ax.set_ylabel(偏差 (mm))ax.set_title(各打印方向×特征 尺寸偏差分布箱线图, fontsize14, fontweightbold)ax.legend()plt.xticks(rotation45, fontsize8)plt.tight_layout()plt.savefig(self.results_dir / boxplot_deviation.png,dpi150, bbox_inchestight)plt.close()def heatmap(self, cap_df):方向×特征 偏差热力图pivot cap_df.pivot(indexorient, columnsfeature, valuesabs_mean_dev)pivot pivot.reindex([平放,竖放,斜放]).reindex(columnscap_df[feature].unique())fig, ax plt.subplots(figsize(10, 5))im ax.imshow(pivot.values, cmapYlOrRd, aspectauto)ax.set_xticks(range(len(pivot.columns)))ax.set_xticklabels(pivot.columns)ax.set_yticks(range(len(pivot.index)))ax.set_yticklabels(pivot.index)for i in range(pivot.shape[0]):for j in range(pivot.shape[1]):ax.text(j, i, f{pivot.values[i,j]:.3f},hacenter, vacenter, fontsize9,colorblack)ax.set_title(方向×特征 平均绝对偏差热力图 (mm), fontsize14, fontweightbold)plt.colorbar(im, axax)plt.tight_layout()plt.savefig(self.results_dir / deviation_heatmap.png,dpi150, bbox_inchestight)plt.close()def cpk_bar(self, cap_df):各方向CPK对比fig, ax plt.subplots(figsize(12, 6))orients [平放,竖放,斜放]features cap_df[feature].unique()x np.arange(len(features))w 0.25cmap {平放:#27AE60,竖放:#E74C3C,斜放:#F39C12}for i, o in enumerate(orients):vals []for f in features:sub cap_df[(cap_df[orient]o) (cap_df[feature]f)]vals.append(sub[Cpk].values[0] if not sub.empty else 0)ax.bar(x (i-1)*w, vals, w, labelo, colorcmap[o], edgecolorwhite)ax.axhline(1.33, colorgreen, ls:, labelA级线1.33)ax.axhline(1.0, colorred, ls--, label可接受线1.0)ax.set_xticks(x)ax.set_xticklabels(features)ax.set_ylabel(Cpk)ax.set_title(各打印方向×特征 过程能力指数对比, fontsize14, fontweightbold)ax.legend(fontsize8)ax.grid(axisy, alpha0.3)plt.tight_layout()plt.savefig(self.results_dir / cpk_bar.png,dpi150, bbox_inchestight)plt.close()def risk_scatter(self, df):风险聚类散点if risk_level not in df.columns:returnfig, ax plt.subplots(figsize(10, 7))levels [安全区,警戒区,高风险区]cmap {安全区:#27AE60,警戒区:#F39C12,高风险区:#E74C3C}for lvl in levels:sub df[df[risk_level]lvl]if sub.empty:continueax.scatter(sub[abs_mean_dev], sub[std_dev],ccmap[lvl], labellvl, s80,edgecolorsblack, linewidths0.5, alpha0.85)for _, r in sub.iterrows():ax.annotate(f{r[orient]}{r[feature]},(r[abs_mean_dev], r[std_dev]),fontsize7, haleft)ax.set_xlabel(平均绝对偏差 (mm))ax.set_ylabel(标准差 (mm))ax.set_title(方向×特征 风险画像聚类, fontsize14, fontweightbold)ax.legend(fontsize9)ax.grid(alpha0.3)plt.tight_layout()plt.savefig(self.results_dir / risk_cluster.png,dpi150, bbox_inchestight)plt.close()def network_plot(self, G):方向-特征关系网fig, ax plt.subplots(figsize(12, 9))pos nx.spring_layout(G, seed42, k利用AI解决实际问题如果你觉得这个工具好用欢迎关注长安牧笛