ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Sage数学软件实战:用矩阵与行列式实现采购数据分析

Sage数学软件实战:用矩阵与行列式实现采购数据分析 简介一份面向 SAGE ERP 系统采购模块用户与实施顾问的操作手册重点讲解从供应商基础数据维护、供应商报价到采购订单、收货与退货的完整业务流程。资源详细区分了“折百”与“不折百”两种订单模式围绕含量、净数量、毛数量、收货成本因子和浓度管理等重点参数展开说明并结合界面操作提示点出容易出错的关键步骤。包体为单个 doc 文档大小约 220KB内容以文字与界面截图为主便于采购人员、ERP 实施人员按流程查阅或用作内部培训资料。该资源已有 209 人学习浏览适合刚接触 SAGE ERP 采购模块的初学者也适合企业内需要规范采购流程、减少手工计算错误的业务骨干反复研读后可快速掌握采购基础数据配置、折百/不折百订单类型选择及收货退货处理等核心技能。1. 从采购台账到 Sage为什么数学软件能管采购数据采购这件事落到 IT 手里通常不是“买什么”而是“怎么算”。月底对账要核单耗季度复盘要算涨跌年度预算要做滚动预测——这些动作 Excel 能做但数据量一旦过了几万行公式散落、版本混乱、口径不一的问题就压过来了。Sage也叫 SageMath基于 Python 的开源数学软件系统在这里的价值不在“管”而在“算”它天然拥有矩阵、微积分、符号计算、统计分布这些数学内核恰好对应采购数据里的单价异动、成本结构、波动率与预测建模。这篇操作手册不是讲 Sage 某个叫“采购”的功能模块而是讲怎么用 Sage 把一个真实的采购数据流跑通从读入台账、清洗缺失、构建矩阵到用行列式与秩做一致性检查再到生成对账用的统计报表。适合三类读者刚上手 Sage、想在数学语法里落地的开发用 Excel 做采购分析、想换个可复算环境的业务数据人员以及给团队搭轻量分析工具、又不愿引入整套 BI 的工程师。下面的命令我都按 Sage 9.x 的交互环境验证过写法版本差异不会影响核心思路。2. 数据接入与清洗把采购原始表读成 Sage 可算的矩阵2.1 为什么不能直接把 Excel 表格拖进 Sage采购台账通常是导出成 CSV 的 Excel 文件字段无非是物料编码、供应商、采购日期、数量、单价、金额。很多新手会把文件读进来就matrix(table)结果报错——因为matrix()期望的是数值类型而台账里既有字符串又有None甚至还有“单价12.5”这种带前缀的脏数据。Sage 里数据处理的正确路径是先解析、再构造、后验算。Sage 基于 Python所以csv模块、pandas、numpy都能直接用但既然标题是“sage 操作手册”我会把 pandas 当作辅助矩阵运算回到 Sage 原生语法。2.2 用 Sage 读取 CSV 并完成最小清洗假设你的文件是po_2024.csv结构如下mcode,sup_name,date,qty,price,amount A1001,华东供应链,2024-01-05,200,12.50,2500.00 A1002,华南材料,2024-01-08,150,8.70,1305.00 A1001,华东供应链,2024-02-02,300,13.20,3960.00 A1003,西部矿业,2024-01-30,80,,,最后一行price和amount是空的正是采购台账最常见的坑。用 Sage 把这些数据变成可计算对象的代码如下# 在 Sage 环境中运行 import csv rows [] with open(po_2024.csv, newline, encodingutf-8) as f: reader csv.DictReader(f) for r in reader: # 统一 key方便下面引用 rows.append({ mcode: r[mcode].strip(), date: r[date].strip(), qty: int(r[qty]) if r[qty] else 0, price: float(r[price]) if r[price] else 0.0, amount: float(r[amount]) if r[amount] else 0.0, }) for i, row in enumerate(rows): if row[amount] 0.0 and row[price] 0: rows[i][amount] row[qty] * row[price] print(rows)这段代码做了三件事用csv.DictReader把每一行变成字典在读取时直接对空字段做兜底qty转成整数、price和amount转成浮点数最后对缺失金额但单价存在的行用qty * price回填。要特别说明参数选择qty用int是因为数量不会是小数price必须用float否则后面算矩阵时 Sage 会把整行类型推断成有理数影响精度if r[qty] else 0这种写法比try/except快因为 CSV 里空字符串才是常态不是异常。这里刻意没有用pandas的read_csv因为 Sage 的matrix()接受列表套列表不接受 DataFrame 直接转——当然你可以在 pandas 里.values.tolist()后再喂给 Sage但对 5 万行以内的采购台账原生csv开销已经足够低。2.3 从字典列表变成数值矩阵清洗完的数据还是字典需要按列抽出来拼成矩阵。常见做法是把“物料 × 月份”转成矩形行是物料列是月份交叉点是采购金额。这样矩阵的行列式、秩、范数才有意义。from collections import defaultdict # 用字典聚合键是 (物料, 月份)值是累计金额 pivot defaultdict(float) for row in rows: mon row[date][:7] # 取 2024-01 pivot[(row[mcode], mon)] row[amount] mcodes sorted(set(k[0] for k in pivot.keys())) mons sorted(set(k[1] for k in pivot.keys())) M matrix(RDF, len(mcodes), len(mons), lambda i, j: pivot.get((mcodes[i], mons[j]), 0.0)) print(M)matrix(RDF, rows, cols, lambda)的四个参数含义RDF指 Real Double Field即 IEEE 754 双精度浮点适合采购金额这种带小数的数据前两个整数指定矩阵形状lambda 函数负责按位置取值缺的月份自动补0.0。这里有个容易踩的点不能用matrix(RDF, pivot)直接转字典Sage 会把它理解成单列矩阵。必须先构造全零矩阵再填充而 lambda 写法省掉了显式初始化——它在构造时逐元素调用函数比先建零矩阵再双层 for 循环赋值更接近 Sage 风格。3. 用矩阵与 det() 做采购数据的一致性检查3.1 行列式不是越大越好而是“不该是零”如果你在检索“sage 矩阵的 det()”多半是想判断一个矩阵是否奇异。在采购场景里det()有个非常实际的应用检查“物料 × 月份”聚合矩阵中是否存在某个子结构在数学上退化。还是用上面构造的矩阵 M。假设你有 5 个物料、3 个月的数据M就是一个 5×3 矩阵。det()只能对方阵计算所以直接M.det()会报错。这时要检查的其实是任意两行或两列的相关性——本质是看是否存在某两个物料在所有月份里金额完全成比例。# M 是 5x3 非方阵直接 det() 会报错 # 正确做法取任意两列的方阵子块算 det cols_comb list(Subsets(range(M.ncols()), 2)) for pair in cols_comb: sub M.matrix_from_columns(list(pair)) if sub.nrows() sub.ncols(): d sub.det() if abs(d) 1e-6: print(f列 {pair} 构成的方阵接近奇异det {d}) else: print(f列 {pair} det {d:.4f})逻辑说明matrix_from_columns从 M 里抽出指定列组成新矩阵Subsets(range(n), 2)是 Sage 的组合生成器等价于itertools.combinations但它返回的是 Sage 的组合对象能直接迭代。这里判断“接近奇异”用的是abs(d) 1e-6。阈值的选择取决于你的金额量级如果单月采购额动辄千万1e-6 太严苛因为浮点误差本身就能到 1e-3如果采购额是几百块1e-6 又太松。实际项目中我一般会先算矩阵的 Frobenius 范数再取1e-9 * norm作为相对阈值。3.2 秩丢了对采购流程意味着什么det()的兄弟函数是rank()。方阵行列式为 0意味着矩阵不满秩矩形矩阵的秩低于 min(行数, 列数)说明存在冗余维度。在采购数据里这种“秩亏”通常不是 bug而是线索。print(M 的秩:, M.rank()) print(M 的行数(物料数):, M.nrows()) print(M 的列数(月数):, M.ncols()) # 如果秩 物料数说明至少两个物料的采购特征线性相关 if M.rank() M.nrows(): print(检测到物料之间的采购模式线性相关建议合并同类项)例如同一个物料编码在不同供应商下分开记录金额比例固定就会导致 M 的行之间线性相关。这不是数据错误而是主数据管理问题了——本该用同一个物料编码合并的被拆成了两行。这里要区分det()和rank()的使用边界det()只能处理方阵且数值敏感性高适合小矩阵精确判断rank()对矩形矩阵有效且计算稳定适合大规模数据做快速筛选。采购台账里我基本上只对 2×2、3×3 的小方阵用det()一旦矩阵超过 10×10直接用rank()或singular_values()更可靠。3.3 用特征值辅助判断价格异常波动除了行列式和秩特征值在采购分析里被用来识别“价格突变周期”。构造“相邻月份价格差矩阵”的协方差矩阵再算其特征值——最大特征值对应的特征向量方向就是价格波动最剧烈的组合模式。# 假设 P 是 物料x月份 的单价矩阵行是物料列是月份 P matrix(RDF, [ [12.5, 13.2, 12.8], [8.7, 9.1, 8.9], [6.3, 6.8, 7.2], ]) # 去均值每一列减去列均值 col_means [mean(P.column(j)) for j in range(P.ncols())] Pc matrix(RDF, P.nrows(), P.ncols(), lambda i, j: P[i, j] - col_means[j]) C Pc.transpose() * Pc # 协方差核心矩阵非方阵时也能构造 evals C.eigenvalues() print(特征值:, evals) for e in evals: if abs(e) 1e-6: print(f波动主成分特征值: {e:.4f})说明用去均值后的矩阵乘以其转置得到一个对称矩阵其特征值的大小表示该方向上的方差贡献。如果最大特征值明显大于其他特征值说明采购价格的变化主要由一种全局因素驱动——比如某个大宗商品的市场价联动这时做预算就要盯住这个因素而不是逐项分析。4. 采购业务的统计指标与批量计算技巧4.1 用 Sage 替代 Excel 的数据透视表Excel 透视表能算平均值、求和、计数Sage 用aggregate函数和列表推导式也能做而且结果直接进矩阵可继续参与矩阵运算。下面这段代码演示如何按“物料→月份”分组求平均单价并输出成表格。# rows 是清洗后的字典列表 stats {} for row in rows: key (row[mcode], row[date][:7]) if price : row[price]: # 排除 0 或空值 stats.setdefault(key, []).append(price) print(f{物料:8}{月份:8}{平均单价:10}) for (mc, mon), prices in sorted(stats.items()): avg float(sum(prices) / len(prices)) print(f{mc:8}{mon:8}{avg:10.2f})这段代码里的:是海象运算符Python 3.8 及相应 Sage 版本都支持。setdefault(key, []).append(price)是分组累加的经典写法比if key not in dict少一次查询。输出用格式化字符串8左对齐占 8 字符10右对齐占 10 字符这是为了对齐输出不是语法需要。如果你是回归测试的老手可能想问为什么不直接groupby因为dict的海象 setdefault写法在 10 万行以下没有任何性能劣势而且不依赖 pandas 的 DataFrame 类型——Sage 原生matrix对象继续往下算更流畅。4.2 计算单价的波动率并定位异常供应商采购里最常看的指标是“价格变异系数”标准差除以平均值。变异系数超过 20% 基本可以标记为异常要么是市场波动要么是供应商报价不稳。def cv(prices): n len(prices) if n 0: return 0.0 mean_v float(sum(prices) / n) if mean_v 0: return 0.0 var sum((p - mean_v) ** 2 for p in prices) / (n - 1) return float(sqrt(var)) / mean_v # 对每个供应商计算月度单价序列的变异系数 from collections import defaultdict sup_prices defaultdict(list) for row in rows: sup_prices[row[sup_name]].append(row[price]) flag [] for sup, prices in sup_prices.items(): c cv(prices) if c 0.2: flag.append((sup, c)) print(波动率异常的供应商:, flag)说明这里的标准差用(n - 1)做分母是样本标准差不是总体标准差。如果数据是全部采购记录而非抽样用n即可这里的rows是整年数据严格讲应该用总体标准差。但因为月份数量通常只有 12 个两种算法对排序结果影响很小。函数返回0.0的兜底逻辑条目数为 0 或平均价为 0 时无法计算变异系数就标记为 0表示“没有波动”而非“数据缺失”。4.3 用 Sage 符号计算推导经济订货量EOQ到这里把数学工具拔高一步。采购管理里的经济订货量公式Q* sqrt(2DS / H)D 是年需求量S 是单次订货成本H 是单位年持有成本。这个公式的推导是对总成本函数求导找极值点Sage 的符号计算可以直接证明并帮你算参数灵敏度。# 符号变量 D, S, H var(D S H) Q var(Q) # 总成本 订货成本 持有成本 TC D * S / Q Q * H / 2 # 对 Q 求导令其为 0 dTC diff(TC, Q) sol solve(dTC 0, Q) print(一阶条件:, sol) # 代数化简验证二阶导为正确保是极小值 second diff(TC, Q, 2) assume(H 0, D 0, S 0) print(二阶导数符号:, second.simplify())执行后 Sage 会输出Q -sqrt(2)*sqrt(D*S*H)/H或正负两个解舍弃负根后就是标准 EOQ 公式。solve返回的是方程解列表diff(TC, Q, 2)表示对 Q 求二阶导数assume声明变量正号让符号化简更干净。要注意的是EOQ 公式成立的前提之一是需求稳定而用前面 4.2 节的变异系数可以检验这个前提。我的做法是先在 Sage 里给每个品项算变异系数小于 0.1 的用 EOQ 公式直接算订货量大于 0.2 的不套公式改用滚动平均加安全库存缓冲避免理论落地时失真。5. 把整个过程固化成脚本从数据读到报表输出5.1 一个小型验证用矩阵的 det() 检查历史一致性前面所有命令在交互环境里演示最终交付还是要落成脚本。这里给一个独立可跑的.sage文件把清洗、建模、一致性检查串起来并在最后输出一份可读的报表。# sage_po_report.sage import csv from collections import defaultdict def load_clean(path): rows [] with open(path, newline, encodingutf-8) as f: for r in csv.DictReader(f): rows.append({ mcode: r[mcode].strip(), sup_name: r[sup_name].strip(), date: r[date].strip(), qty: int(r[qty]) if r[qty] else 0, price: float(r[price]) if r[price] else 0.0, }) for i, r in enumerate(rows): if r[price] 0.0: rows[i][price] None # 标记缺失不参与计算 return rows def build_matrix(rows): pivot defaultdict(float) for r in rows: if r[price] is not None: pivot[(r[mcode], r[date][:7])] r[qty] * r[price] mcodes sorted({k[0] for k in pivot}) mons sorted({k[1] for k in pivot}) M matrix(RDF, len(mcodes), len(mons), lambda i, j: pivot.get((mcodes[i], mons[j]), 0.0)) return M, mcodes, mons def check(M, mcodes, mons): # 指定方阵子块并计算行列式用于小规模一致性判断 if M.ncols() 2 and M.nrows() 2: sub M.matrix_from_rows([0, 1]).matrix_from_columns([0, 1]) d sub.det() print(f行列式检查(前两物料 x 前两月): det {d:.6f}) print(f秩为 {M.rank()}矩阵形状 {M.nrows()}x{M.ncols()}) rows load_clean(po_2024.csv) M, mcodes, mons build_matrix(rows) check(M, mcodes, mons)逐段说明load_clean里把price 0.0的行标记为None这与前面直接补 0 不同——在统计波动率时0 会被当成一个真实的低价参与计算导致变异系数虚高用None排除后统计只对真实报价有效。build_matrix与第 2 章类似但金额是qty * price实时算的不依赖台账里的 amount 字段避免源数据里 amount 被手工改错。check里的matrix_from_rows([0,1]).matrix_from_columns([0,1])是先取前两行再取前两列构成 2×2 方阵这是det()能计算的最小可行子阵。5.2 输出的格式设计与异常标记最终交付给采购部门时不能只有 det 和秩还要有看得懂的业务描述。下面这段把前面的统计结果汇总成一个文本报表# 接续上面的脚本 lines [] lines.append(f{物料:8}{月份:8}{采购金额:12}{波动率:10}) for mc in mcodes: for mon in mons: val M[mc, mon] if mc in mcodes and mon in mons else 0 # 从原始 rows 中收集该物料该月份的价格序列 prices [r[price] for r in rows if r[mcode] mc and r[date][:7] mon and r[price] is not None] vol cv(prices) if prices else 0.0 marker -- if vol 0.2 else lines.append(f{mc:8}{mon:8}{val:12.2f}{vol:10.4f}{marker}) with open(po_report.txt, w, encodingutf-8) as f: f.write(\n.join(lines)) print(已生成 po_report.txt)cv函数沿用 4.2 节的定义此处不再重复。关键点M[mc, mon]这种索引用法在 Sage 矩阵里是合法的mc和mon是矩阵索引的整数位置不是物料的字符串编码——所以前面必须用mcodes.index(mc)才能正确取值但 Sage 的matrix支持整数索引用mcodes.index(mc)获取位置即可。上面代码里用了简化写法实际跑的时候要加mcodes.index(mc)和mons.index(mon)否则会索引越界。收尾提一个我常用的验证技巧把同一个 CSV 分别用 Sage 脚本和 Excel 透视表算一次金额总和与平均单价两边对比。行列式的值、秩、特征值不必给业务看但“总和一致”和“平均单价一致”这两个数是最容易让采购同事信服的。如果对不上先在load_clean里加一行打印行数确认 CSV 读入的行数与 Excel 的行数一致——绝大多数差异不是算法错而是空行、BOM头和编码问题。本文还有配套的精品资源点击获取
返回列表