资讯详情 WEKA数据预处理实战:ARFF格式、CSV转ARFF与离散化避坑
📅 2026/10/27 11:51:01
简介这份文档面向刚接触数据挖掘、希望快速上手 WEKA 的读者围绕 WEKA 平台的基础操作与数据准备展开适合课程实验、入门练习和算法初探场景。压缩包内共 1 个 doc 文件约 172KB篇幅集中便于按主题通读或随查随用。内容从 WEKA 的开放接口与主要功能切入介绍数据预处理、分类、回归、聚类、关联规则及可视化等模块说明实例、属性、关系等核心术语并重点梳理 ARFF 文件的头信息、属性声明、数据类型与数据区结构同时提及 CSV 等外部数据的转换准备可帮助读者理解 WEKA 的数据组织方式并完成基础操作。已有 792 人学习说明其对入门者具有较稳定的参考价值对于需要建立数据挖掘工具使用框架、后续开展分类聚类实验的读者这份文档可作为轻量级起步材料。1. 搜「weak 操作入门」的人最后装的都是 WEKA十有八九是拼错了想找的是 WEKA不是 weak。这个跑数据挖掘的老家伙是一套开源机器学习工作台界面朴素但预处理、分类、回归、聚类、关联规则、可视化这些活它都能接而且不用写一行代码就能把一份 CSV 从头跑到出结果。刚上手数据挖掘、手上只有 Excel 和一份业务表的人最适合从它切入不用先折腾环境打开就能看见数据长什么样、分布偏不偏、离散化之后分箱落在哪。它把「数据挖掘」这件事拆成了看得见的按钮和参数代价是它对数据格式有洁癖——喂进去的东西不合规界面就直接报错给你看。2. ARFF 文件格式三段结构、四种类型和三条硬规矩WEKA 只认一种原生格式ARFFAttribute-Relation File Format本质是一份 ASCII 文本。它把所有数据集都看成二维表一行是一个实例Instance相当于统计里的一个样本、数据库里的一条记录一列是一个属性Attribute相当于一个变量或一个字段整张表叫一个关系Relation。这套术语听着绕落到文件里其实就是三段结构看懂之后你手改数据都不慌。2.1 头信息、属性声明、数据体三段各管什么一份合法 ARFF 去掉%开头的注释行之后只剩两大部分头信息Head information和数据信息Data information。头信息里第一行有效内容声明关系名接着每个属性一行attribute最后用data标记把数据体切开。拿 WEKA 自带的 weather 数据集举例结构是这样的% 这是一个注释行WEKA 读取时会整行忽略 relation weather attribute outlook {sunny, overcast, rainy} attribute temperature real attribute humidity real attribute windy {TRUE, FALSE} attribute play {yes, no} data sunny,85,85,FALSE,no sunny,80,90,TRUE,no overcast,83,86,FALSE,yes rainy,70,96,FALSE,yes rainy,68,80,FALSE,yes rainy,65,70,TRUE,no overcast,64,65,TRUE,yes sunny,72,95,FALSE,no sunny,69,70,FALSE,yes rainy,75,80,FALSE,yes sunny,75,70,TRUE,yes overcast,72,90,TRUE,yes overcast,81,75,FALSE,yes rainy,71,91,TRUE,norelation后面跟关系名字符串里如果有空格必须用单引号或双引号包起来比如relation bank data。声明顺序不是装饰attribute出现的先后顺序直接决定了data区里逗号分隔的每一列对应哪个字段。weather 里 humidity 是第三个声明的属性所以数据区第三列 85、90、86、96 就是 humidity 的值你把两条attribute行调换位置整份数据的语义就全错了而且 WEKA 不会报错它只会安静地算出一堆垃圾结果。还有一条容易被忽略的规则最后一个声明的属性被当作 class 属性也就是分类或回归任务默认的目标变量。weather 里 play 排在最后所以它自动成为类别列。你把标签列放在第一列Explorer 打开后分类面板的目标变量就是空的得手动去设凡是流程里写死了「最后一列是 class」的脚本都会跟着崩。2.2 四种数据类型numeric、nominal、string、dateWEKA 支持的属性类型只有四种认清它们比记命令重要因为后面 90% 的报错都源于类型判断错了。类型写法含义典型场景常见误用numeric或real连续数值年龄、收入、温度把编码当数值算均值{a,b,c}分类nominal性别、是否已婚取值里有逗号没加引号string自由文本备注、地址拿去跑分类器date [format]日期时间交易时间格式串和实际值对不上日期类型的声明是attribute name date [date-format]方括号里是解析和显示格式省略时默认按yyyy-MM-ddTHH:mm:ss这种 ISO-8601 组合解析。格式串本身要用单引号包住内部的字符比如attribute ts date yyyy-MM-dd HH:mm:ss。这里有个血泪经验数据区里的日期字符串必须严格符合声明里的格式多一个空格、少一个前导零整行就会解析失败报出来的错还只说「line N: 无法解析」不告诉你到底哪一位对不上。稳妥做法是先把日期列当string读进来用 Filter 转成 date转成功了再回去改声明。nominal 属性的坑更隐蔽。取值列表里只要出现逗号、空格、花括号、引号这类字符单个取值就得加引号写成{New York,Los Angeles}。不加引号的话 WEKA 会把一个值切成两个于是你得到一列取值数翻倍的 nominal 属性而且不报错。2.3 缺失值与稀疏格式?和花括号两种写法数据体里的缺失值统一写?不要写 NULL、NA、空字符串WEKA 只认问号这一个符号。空字段和?在语义上不同前者会被当成空字符串后者才是真正的 missing。当你的数据极度稀疏——比如几千个商品 ID 做列、每行只有十几个非零值——用常规的逗号分隔会写出一堆零文件体积爆炸。这时用稀疏格式一行写成{0 1, 5 3.5, 12 yes}花括号里是「属性下标 值」的键值对下标从 0 开始没列出来的属性一律视为 0 或缺失。稀疏格式和普通格式不能在同一份文件里混用data之后要么全是普通行要么全是花括号行。2.4 手工修改 ARFF 的三条硬规矩实际项目里改 ARFF 是家常便饭尤其是把children这种只取 0、1、2、3 的数值列改成 nominal。改之前记住三条第一改类型声明之前先确认这一列所有取值都在新取值集合里。声明成{0,1,2,3}但数据里冒出一个 4WEKA 直接报「value not declared」整份文件打不开你得回去数哪一行越界了。第二改 nominal 的取值列表顺序会影响内部编码。WEKA 内部把 nominal 值映射成 0、1、2 的索引顺序一变之前训好的模型里那些阈值就全错位了。要么一次定死不再改要么改了之后重训。第三改完一定要重新打开验证。Explorer 里选中那一列区域 6 的 Type 从 Numeric 变成 Nominal说明改对了如果还是 Numeric多半是你改的是副本或者文件编码带了 BOM 导致第一行relation没被识别整份头信息被当成了数据。提示ARFF 用 UTF-8 保存别用带 BOM 的 UTF-8。BOM 藏在文件开头肉眼看不见WEKA 读到之后会把第一个relation当成非法行跳过然后属性数量对不上报错信息驴唇不对马嘴。3. CSV 转 ARFF 的三条路线命令行、GUI 和 Java API现实里的数据几乎不会是 ARFF。业务系统导 Excel、统计脚本吐 CSV、数值计算环境存矩阵这些格式 WEKA 都能接但都得先转一道。转换本身不难难在转换过程中类型判断错了你还不知道等到建模阶段才发现某一列被当成了字符串。下面三条路线按使用频率排各有各的适用边界。3.1 路线一Simple CLI 里的一行命令启动 WEKA 主程序GUI 下方有一排按钮点进 Simple CLI 模块窗口最下方是输入框上方那块是只读输出区敲不进去字。直接输入java weka.core.converters.CSVLoader bank-data.csv bank-data.arff回车执行输出区会打印转换过程中的信息。这行命令干的事情是调用weka.core.converters.CSVLoader读入 CSV默认把第一行当表头解析成属性名把每个字段自动推断成 numeric 或 nominal然后把结果以 ARFF 格式打到标准输出重定向到文件。因为是在 WEKA 运行时内部执行不用额外指定 classpath。自动推断的规则是整列都能解析成数字就当 numeric否则当 nominal。这条规则在两种情况下会翻车。一是 ID 列1001, 1002, 1003全数字被当成 numeric于是 WEKA 认为这个变量有连续取值算均值算方差毫无意义。二是本该是 nominal 的数字编码列比如sex列存的是 0/1被推断成 numeric后面所有只接受 nominal 的算法全用不了。这两种情况都要在转换后手工改声明或者用后面 3.3 的 API 显式指定。CSVLoader 还支持几个实用参数值得记一下参数作用什么时候用-H声明文件没有表头数据第一行就是记录-M 符号指定缺失值标记源文件用\N、NA表示空-F 列号强制某些列按 nominal 读数字编码的分类列-D 格式指定日期解析格式带时间戳的日志表-S输出稀疏格式高维稀疏矩阵-F的参数写法是逗号分隔的列索引支持区间比如-F 2,5,7-10。注意列号从 1 开始数和 Explorer 里属性列表左边显示的数字一致。3.2 路线二Explorer 打开再另存不想碰命令行的话在 Explorer 的 Preprocess 面板点 Open file文件类型下拉框选 CSV选中文件打开WEKA 会走同一套推断逻辑。打开之后点 Save类型选 ARFF另存即可。这条路的额外价值是你能立刻在区域 5 看到每一列被推断成了什么类型在区域 6 看到选中列的摘要Type、Missing 数量、Distinct 数量、Min/Max、均值标准差类型错了当场就能发现。前置环节也有讲究。Excel 的 xls 文件可以放多张工作表而 WEKA 一次只认一张表所以必须切到目标工作表另存为 CSV遇到「只保存当前工作表」的提示点确定。数值计算环境里存的矩阵用csvwrite(bank-data.csv, matrixname)这类命令导出导出的文件通常没有属性名得先用文本编辑器手工补一行表头属性名个数必须和列数严格一致用逗号分隔少一个多一个都会让后面的解析整体错位。补表头这件事看着琐碎它是新手最容易翻车的一步。源文件没有表头时WEKA 会把第一行数据当成属性名于是你的属性名变成25,43,1这种非法字符串属性声明直接失败更糟的情况是第一行恰好能解析于是第一条样本静默消失你拿 999 条数据训练却以为是 1000 条。3.3 路线三Java API 批量转换并锁定类型数据集一多命令行一行行敲就不现实了。用 Java API 写个小工具把类型判断规则固化下来以后每次转换都跑同一套逻辑口径才不会漂。下面这段可以直接编译运行把weka.jar加进 classpathimport java.io.File; import weka.core.Instances; import weka.core.converters.ArffSaver; import weka.core.converters.CSVLoader; public class Csv2Arff { public static void main(String[] args) throws Exception { File in new File(args[0]); File out new File(args[1]); CSVLoader loader new CSVLoader(); loader.setSource(in); loader.setFieldSeparator(,); // 分隔符分号表改成 ; loader.setNoHeaderRowPresent(false); // 第一行是表头 loader.setMissingValue(?); // 源文件的缺失标记 loader.setNominalAttributes(2,5,7-10); // 强制这些列按分类读 Instances data loader.getDataSet(); // 一次性读入内存 ArffSaver saver new ArffSaver(); saver.setInstances(data); saver.setFile(out); saver.setDestination(out); // 3.6 之后必须显式设置 saver.writeBatch(); System.out.println(data.numInstances() 条 / data.numAttributes() 列); } }编译运行两行命令javac -cp weka.jar Csv2Arff.java java -cp .:weka.jar Csv2Arff bank-data.csv bank-data.arff逻辑很直白CSVLoader负责读setNominalAttributes把「看起来像数字但其实是编码」的列钉死成 nominalgetDataSet()一次性把整份数据装进Instances对象ArffSaver负责写。三处参数最值得改setFieldSeparator遇到分号或制表符分隔的表必须换setMissingValue要和源文件一致源文件用\N你就写\NsetNominalAttributes的列号列表是从业务语义出发的写完最好回头看一眼生成的 ARFF 头信息确认无误。getDataSet()是批量读取整份数据驻留内存百万行级别的表需要调大堆内存java -Xmx4g -cp .:weka.jar Csv2Arff ...。如果你的数据真到了这个量级更合适的是用增量转换边读边写但那需要自己实现Loader和Saver的流式接口日常项目一般用不上。3.4 转换之后必须做一次列数校验不管走哪条路线转完都别急着建模。写个十几行的校验脚本把每行的字段数和attribute声明数对一遍能挡掉绝大多数低级问题import csv, sys def check(path): n_attr, in_data, bad 0, False, [] with open(path, encodingutf-8-sig) as fh: # utf-8-sig 顺手吃掉 BOM for lineno, raw in enumerate(fh, 1): line raw.strip() if not line or line.startswith(%): # 空行与注释跳过 continue low line.lower() if low.startswith(attribute): n_attr 1 elif low.startswith(data): in_data True elif in_data and not line.startswith({): cols next(csv.reader([line], skipinitialspaceTrue)) if len(cols) ! n_attr: bad.append((lineno, len(cols))) return n_attr, bad n, bad check(sys.argv[1]) print(f属性数 {n}错行列数 {len(bad)}) for lineno, cnt in bad[:10]: # 只打印前 10 条 print(f 第 {lineno} 行有 {cnt} 列)这段脚本按行扫遇到attribute计数遇到data之后开始比对列数csv.reader负责处理引号包裹的取值。有个已知局限它按双引号规则解析而 ARFF 允许单引号遇到含逗号的单引号取值可能误判所以报出来的错行要人工复核一眼。跑通之后你会拿到一个很实在的数字——如果有错行说明源 CSV 里有未转义的逗号藏在字段内部那才是真正要修的地方。4. Explorer 八大区域实战删列、改类型、离散化Explorer 是 WEKA 里用得最多的模块界面初看密密麻麻按功能切成八块之后就清楚了。区域 1 是一排选项卡切换 Preprocess、Classify、Cluster、Associate、Select attributes、Visualize 六类任务面板日常九成时间待在 Preprocess。区域 2 是常用按钮组打开文件、打开 URL、连数据库、生成随机数据、撤销、编辑、保存。区域 3 是 Filter 选择区Choose旁边那个文本框显示当前选中的过滤器及其参数点它弹窗改参数。区域 4 显示数据集概况关系名、实例数、属性数。区域 5 列出所有属性左边数字是列下标前面有勾选框上方一排按钮做批量勾选All、None、Invert、Pattern。区域 6 是选中属性的摘要数值型和分类型的摘要项目不同。区域 7 是直方图默认按 class 着色。区域 8 是状态栏右侧那只鸟在动就说明任务还在跑右键可以手动触发垃圾回收。4.1 删掉 ID 列和明显无用的字段业务表里通常有一两列是纯标识符比如客户编号、订单流水号。这类列对挖掘任务毫无贡献数值型还会把均值方差算歪第一步就该删。在区域 5 勾选id点区域 2 的Remove列就没了。误删了按Undo能找回来但 Undo 只回退一步连续删三列就找不回第一列了所以删之前先 Save 一份。同样的效果用 Filter 也能做而且可复现java -cp weka.jar weka.filters.Filter \ -F weka.filters.unsupervised.attribute.Remove -R 1 \ -i bank-data.arff -o bank-data-no-id.arff-R 1表示删第 1 列。要删多列写-R 1,5,7-9反过来保留指定列用-V。weka.filters.Filter这个入口是通用的-F后面跟任意过滤器的完整类名加参数所有预处理都能这么串。4.2 类型纠偏把只有四个取值的数值列改成 nominalbank-data 数据集里有三个数值列age、income、children。其中children只取 0、1、2、3 四个值本质上是分类变量。有些算法比如只处理 nominal 的朴素贝叶斯变体要求所有属性都是分类型这时候就得改声明。最简单的改法是在文本编辑器里把这一行attribute children numeric改成attribute children {0,1,2,3}保存回 Explorer 重新打开选中children区域 6 里 Type 显示 Nominal 就说明改对了。这一步看着粗糙但比走 Discretize 过滤器更精确——你明确知道应该分成几类、边界在哪而不是让算法替你猜。4.3 Discretize 的参数-B、-M、-R 到底怎么定age和income这种真正连续的变量要变成分类型就得靠离散化。区域 3 点Choose在过滤器树里逐级找到weka.filters.unsupervised.attribute.Discretize点中后文本框会显示类似Discretize -B 10 -M -0.1 -R first-last的默认参数串不同小版本显示略有差异。点这个文本框弹出参数窗口逐项改。参数含义默认建议-B分箱数量10业务可解释的段数36-M用 MDL 准则自动决定箱数关样本量大时开-R作用的属性范围first-last明确写列号-D等频分箱而非等宽关分布严重偏斜时开-R这一项一定要改。默认first-last会把所有数值列都离散化包括你不想动的列而且如果 class 属性恰好是数值型它也会被一起切碎后面的分类任务直接失效。bank-data 里需要离散化的是第 1 列和第 4 列区域 5 属性名左边的数字就是列号所以-R填1,4。计划各分 3 段-B改成 3其余不动点 OK 回到主界面再点Choose右边的Apply区域 5 里age和income的 Type 立刻变成 Nominal。等宽和等频的差别值得单独说一句。等宽是把数值范围均分成若干段遇到收入这种长尾分布可能 95% 的样本全落在第一个箱里等频是让每个箱里的样本数大致相等分布再偏也能切匀代价是箱的边界变成难看的浮点数业务上不好解释。选哪个取决于下游是给业务看还是给模型用。4.4 把(-inf-34.333333]换成看得懂的标签Discretize 生成的 nominal 取值是区间表达式形如(-inf-34.333333]看着头疼而且到报告里没法交差。改法是保存 ARFF 之后用文本编辑器打开把每一处区间字符串统一替换成业务语义的标签比如(-inf-34.333333]全部替换成0_34(34.333333-56.666667]替换成35_56依次类推。替换要注意两点。一是区间表达式里含有括号、减号、点号某些文本编辑器会把这些当正则元字符务必用「纯文本替换」而不是正则替换否则可能只替换掉一部分。二是替换后的标签不能重名也不能和别的 nominal 属性的取值冲突——虽然 ARFF 不禁止但做可视化时图例会糊成一片。5. 避坑与排查五条反复出现的翻车记录预处理这条链路上报错信息普遍不友好WEKA 经常只丢一句「Problem reading file」就没了。下面五条是反复出现、且每次都要重新想一遍的问题记下来能省不少时间。5.1 现象实例数比预期少一条属性名变成了一堆数字原因源 CSV 没有表头CSVLoader 默认把第一行当属性名解析于是第一条样本被吃掉属性名变成25、43、1这种非法字符串。解决转换前给源文件补一行表头或者在命令行加-H让加载器自己生成Attribute0、Attribute1。补表头更推荐属性名有意义后面看摘要和建模型都省事。补完之后重新跑一遍 3.4 的校验脚本确认实例数对得上。5.2 现象分类面板里找不到目标变量或者目标变量选错了列原因WEKA 默认把最后一个声明的属性当 class而源数据里标签列放在了中间或者干脆没有标签列。解决两个方向。要么在 ARFF 里把标签列挪到最后同时调整data区每一列的逗号顺序这一步很容易漏要么在 Preprocess 面板底部那个None下拉框里手动指定 class 属性。手动指定更安全因为它不改文件结构。要提醒的是手动指定的设置不会自动保存到 ARFF 里下次重新打开又变回默认所以脚本化流程里最好显式写data.setClassIndex(n)。5.3 现象Apply 之后报「attribute is not numeric」或者离散化没生效原因-R指定的列里混进了 nominal 属性Discretize 只处理数值列遇到分类型直接抛异常。另一种情况是你选的过滤器并不是 DiscretizeChoose树点偏了一格参数看着像但类名不对。解决先看区域 3 文本框里的完整类名是不是weka.filters.unsupervised.attribute.Discretize再对照区域 5 的数字确认-R里列的每一列 Type 都是 Numeric。改完之后记得点Apply才真正作用到数据集上只改参数不点 Apply界面不会有任何变化很容易误以为参数没生效。5.4 现象设了-B 3结果分出来 5 个箱原因-M打开了 MDL 准则此时-B不再是固定箱数而是箱数的上限MDL 会在这个上限内自己挑一个它认为最优的数量。解决要固定箱数就把-M关掉。反过来如果你希望算法自己决定切几段就别纠结-B的实际效果接受它给出的结果然后在业务层面检查每个箱的样本量是否够——某个箱里只有两三条样本说明这个分箱对模型没有任何信息量。5.5 现象大文件读到一半抛 OutOfMemoryError或者中文列名变乱码原因前者是整份数据一次性载入内存默认堆内存不够后者是文件编码不是 UTF-8或者带了 BOM。解决内存问题加-Xmx参数命令行写成java -Xmx4g -cp weka.jar ...图形界面则改启动脚本里的 JVM 参数。真要处理超出内存的数据就得换增量加载的路子或者先在数据库侧做聚合。编码问题用不带 BOM 的 UTF-8 重新保存然后把属性名里的中文尽量换成英文短名——不是 WEKA 不支持中文而是中文列名在导出报告、跨工具传递时到处是坑早换早省心。6. 把预处理固化成流水线命令行串起来 FilteredClassifier图形界面点着顺手但它有个致命问题不可复现。同一个人隔一周再点一遍-R可能填成1-4-B可能顺手改成 5最后模型效果对不上你还得回头猜是哪一步不一样。我的做法是先用 Explorer 把参数试出来试好之后立刻翻译成命令行固化成脚本。#!/usr/bin/env bash set -euo pipefail # 任何一步失败立刻停别带着错数据往下跑 JAR/opt/weka/weka.jar # 换成你本机的路径 # 1) CSV 转 ARFF先把编码列钉成 nominal java -cp $JAR weka.core.converters.CSVLoader \ -F 2,5,7-10 bank-data.csv step1.arff # 2) 删掉 ID 列 java -cp $JAR weka.filters.Filter \ -F weka.filters.unsupervised.attribute.Remove -R 1 \ -i step1.arff -o step2.arff # 3) 只对第 1、4 列做等宽离散化各分 3 箱 java -cp $JAR weka.filters.Filter \ -F weka.filters.unsupervised.attribute.Discretize -B 3 -R 1,4 \ -i step2.arff -o step3.arff三步各自独立、每步落一个中间文件出问题时能精确知道是哪一步坏的这比一步到位的长管道好排查得多。set -euo pipefail那行别省WEKA 的过滤器在某些情况下返回码是 0 但输出文件是空的光靠返回码判断不住所以脚本末尾最好再加一句产物检查——文件存在且非空才算过。更值得花时间的是第二件事把预处理和模型绑在一起。很多人先在 Preprocess 面板把数据离散化再切到 Classify 面板训练训练完保存模型上线时拿一份新的原始数据直接丢给模型打分。这里有个隐蔽的不一致——模型是在离散化之后的数据上训的而新数据没走同一个离散化箱的边界对不上打出来的分数全是错的而且不会报错只是效果莫名其妙地差。WEKA 给的解法是FilteredClassifier把过滤器包在分类器内部训练和预测时自动走同一套预处理。import weka.classifiers.meta.FilteredClassifier; import weka.classifiers.trees.J48; import weka.filters.unsupervised.attribute.Discretize; import weka.core.Instances; import weka.core.converters.ConverterUtils.DataSource; DataSource src new DataSource(step2.arff); Instances data src.getDataSet(); data.setClassIndex(data.numAttributes() - 1); // 最后一列是标签 Discretize dis new Discretize(); dis.setAttributeIndices(1,4); dis.setBins(3); FilteredClassifier fc new FilteredClassifier(); fc.setFilter(dis); // 预处理挂进模型 fc.setClassifier(new J48()); // 分类器本体 fc.buildClassifier(data);这样保存下来的模型文件里带着过滤器参数预测时喂原始数据进去内部会先跑一遍离散化再分类。参数口径这件事一旦落到代码里就不需要靠人的记忆维持了。参数速查留一份方便回来对环节参数我常用的取值CSVLoader-F强制 nominal 列按业务语义手动列不用自动推断Remove-R删除列 /-V保留列删 ID 类字段Discretize-B箱数、-R列范围、-MMDL-B 36-R写死列号FilteredClassifiersetFilter/setClassifier预处理与模型绑定保存从那以后我每次做数据预处理都强制自己走一遍「GUI 试参 → 命令行固化 → FilteredClassifier 打包」这三步哪怕只是一个几十行的玩具数据集。因为踩过一次上线后分数全歪、排查两天才发现是新数据没走离散化的坑那种感觉不想再体验第二次。希望帮到你。本文还有配套的精品资源点击获取