ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

WEKA入门与ARFF预处理避坑:CSV转换、Explorer离散化实战

WEKA入门与ARFF预处理避坑:CSV转换、Explorer离散化实战

简介:《WEKA操作入门(数据挖掘)》是一份面向数据挖掘初学者与机器学习入门者的单篇文档,重点解决WEKA平台初次使用时术语陌生、数据格式不熟悉、功能模块无从下手等问题。内容围绕WEKA这一开放数据挖掘工作平台展开,涵盖实例、属性、关系等核心概念,以及ARFF文件的头信息与数据信息结构、属性声明顺序、数值型/分类型/字符串型/日期型等数据类型说明,并延伸到CSV数据准备与格式转换等常见入门环节。压缩包仅含1个doc文件,约172KB,篇幅精炼,适合作为随查随用的操作型笔记。已有794人学习,说明其在入门阶段具有一定参考价值;读者可借此快速建立WEKA的数据观与操作路径,理解预处理、分类、回归、聚类、关联规则和可视化等模块的基本定位,为后续上机实验和算法实践打好基础。

1. 从一份 Excel 客户表说起:weak 操作入门的真实起点

很多人第一次搜的是「weak 操作入门」,拼写错了,正确写法是 WEKA——一个不需要写代码就能把数据挖掘全流程跑通的开源工作台。手上有一份几百行的客户表,想看看哪些人更可能响应下一次营销,用脚本要么环境配不齐,要么调参调到怀疑人生;WEKA 装完点开就是界面,打开 CSV、选算法、点 Start,混淆矩阵直接出在右侧。它把预处理、分类、回归、聚类、关联规则和可视化塞进同一个窗口,算法清单长到翻不完,适合两类人:一是要在一两天内出一份能讲给别人听的分析结论的从业者,二是想拿真实数据把课堂上的算法名词跑出实感的学生。代价是它的数据格式和界面逻辑跟主流脚本库完全不同,第一道坎就卡在 ARFF 上。

2. ARFF 文件拆解:三段式结构决定了后面所有报错

ARFF 是 WEKA 唯一认的原生格式,全称 Attribute-Relation File Format,本质是一个纯 ASCII 文本文件。它不像 JSON 那样有嵌套,也不像 CSV 那样只有数据,而是在数据前面硬塞了一段「说明书」。这段说明书是 WEKA 判断每一列是什么、能不能拿来做分类的唯一依据,所以任何一处写错,后面所有算法都会以奇怪的方式失败——不是报错,而是悄悄把某一列当成类别,或者把数值列当成了字符串。

2.1 关系声明、属性声明、数据段

去掉以%开头的注释行之后,整个 ARFF 文件只有两块:头信息(Head information)和数据信息(Data information)。头信息负责声明关系名和每一个属性,数据信息从@data这一行开始,后面全是逗号分隔的记录。

关系声明必须是第一个有效行,格式是@relation <relation-name>。这个名字只是给数据集起个标签,方便在界面上认,不影响计算。如果名字里有空格,必须用英文单引号或双引号包起来,写成@relation 'bank data'。中文名在部分版本上会因为编码问题被截断,我一般直接用英文小写加下划线。

属性声明用@attribute开头,一个属性一行,顺序极其重要。这个顺序就是数据段里列的顺序:第三个被声明的属性,对应的就是每一行里第三个逗号分隔的值。同时,最后一个被声明的属性,在分类和回归任务里被默认当作目标变量(class)。这条默认规则是新手最容易忽略的地方——很多人把 id 放在最后一列,然后疑惑为什么分类结果是按 id 分布的。

数据段的每一行是一条实例(Instance),对应统计学里的一个样本、数据库里的一条记录;每一列是一个属性(Attribute),对应一个变量或字段。整个表格构成一种关系(Relation)。缺失值不用留空,写一个问号?占位,否则解析器会在该位置错位,把后面的值整体左移一列,这种错位不会报错,只会让你在结果里看到一堆莫名其妙的类别。

提示:写完 ARFF 之后,先看数据段每行的逗号数量是否等于属性数减一。这个土办法能挡住八成以上的错位问题。

2.2 四种数据类型,以及 date 的真实写法

WEKA 支持的属性类型只有四种,没有第五种:

类型写法含义典型用途常见误用
numeric数值型,连续的实数年龄、收入、温度把编码后的类别当成数值喂给算法
{a,b,c}名义型(nominal),枚举取值性别、地区、是否响应取值拼写与数据段不一致
string字符串型,不定长文本备注、地址原文拿它当类别列做分类
date [格式]日期时间型交易时间、注册日期格式串与数据实际写法对不上

string这个类型要特别小心:它只能用于承载文本,不能作为分类任务的目标变量。如果最后一列是string,点 Start 时 WEKA 会明确告诉你类别属性必须是名义型。同理,numeric的列也不能直接当类别,想做分类就得先离散化,或者确认这一列本来就是标签编码。

日期型的完整写法是@attribute <name> date [<date-format>],方括号里是解析和显示用的格式串,默认是 ISO-8601 的yyyy-MM-ddTHH:mm:ss。这里有个硬约束:数据段里出现的日期字符串必须严格符合声明里的格式,多一个空格、少一个前导零都会让整行被判定为解析失败。英文字母大小写也要对齐,yyyy-MM-dd和yyyy-mm-dd在格式串里含义完全不同,后者里的mm指分钟。

% 日期属性的两种声明方式 @attribute signup_date date @attribute signup_date date "yyyy-MM-dd" @data 2023-04-01T09:30:00,... 2023-04-01,...

上面第一行用默认格式,数据段就必须带T和时分秒;第二行显式指定了只到日的格式,数据段写纯日期才合法。两种混用是批量导入时最常见的翻车点,尤其在 Excel 里日期被自动转成序列号之后。

2.3 手写一个最小可用的 ARFF 并自检

拿一份天气数据做最小样例,五列十四行,最后一列是是否出门:

% weather.arff —— 最小可用示例,% 开头是注释,WEKA 直接忽略 @relation weather @attribute outlook {sunny, overcast, rainy} @attribute temperature numeric @attribute humidity numeric @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,no

这份文件里outlook和windy是名义型,temperature和humidity是数值型,play是最后一列,自动成为类别。注意名义型的取值列表里不能有空格,如果某个取值本身含空格,比如inner city,必须写成{'inner city',rural}这种带引号的形式,而且数据段里对应的值也要加同样的引号。

自检清单按顺序走一遍,比事后调试省时间:

  1. 第一行有效内容是不是@relation,且只有一个。
  2. @attribute的行数是否与数据段每行的字段数完全一致。
  3. 每个名义型属性的取值集合,是否覆盖了数据段里出现过的所有值,且没有多余值。
  4. 最后一列是不是你真正想预测的那一列;如果不是,在 Explorer 的 Preprocess 面板顶部把 class 下拉框改到正确的列上。
  5. 文件编码用 UTF-8 无 BOM,BOM 头会让第一行的@relation解析失败。

把这五条走完,ARFF 这一关基本就不会再卡人。真正耗时间的往往不是格式本身,而是从别的软件导出来的数据里那些看不见的空格和表头。

3. 数据准备:CSV、Excel、Matlab 三条转换路径

现实里的数据几乎不可能是 ARFF。它可能是业务系统导出的 CSV,可能是财务给的 Excel,也可能是仿真跑完存在 Matlab 工作区里的矩阵。WEKA 对这三种来源都有路径,但每条路径的坑不一样,选错了会在后面反复返工。

3.1 CSV 转 ARFF:CSVLoader 与两个必调参数

CSV 是最省事的一种,因为 WEKA 自带CSVLoader这个转换器。打开 WEKA 主程序后,点界面下方的Simple CLI按钮,会弹出一个只有底部能输入的命令行窗口,直接敲:

# 在 WEKA 的 Simple CLI 窗口中执行,不需要写 -cp java weka.core.converters.CSVLoader bank-data.csv > bank-data.arff

如果是在系统终端里跑,jar 包不在 classpath 上,就得补上路径:

# Linux / macOS 下从安装目录执行,注意 weka.jar 的实际位置 java -cp weka.jar weka.core.converters.CSVLoader bank-data.csv > bank-data.arff

命令的含义很直白:CSVLoader读入第一个参数指定的 CSV,把转换结果打到标准输出,再用重定向写进 ARFF 文件。这里有两个参数值得记住。一个是分隔符,默认是逗号,遇到分号分隔的欧洲格式文件要加-D ";",否则整行会被当成一个字段。另一个是表头处理,较新的版本提供-H选项,强制把第一行当作属性名;如果你的 CSV 第一行是数据而不是表头,不加这个参数就会把第一条记录的值当成列名,后面所有类型推断全乱。

转换完成后别急着用,一定打开生成的文件看一眼属性声明。CSVLoader 的类型推断规则是:能解析成数字的列给numeric,其余给名义型并把所有出现过的值枚举出来。这个规则在多数情况下没问题,但有两类列会翻车——一是形如007的编号列,会被当成数值,前导零消失;二是本该是数值但混进了N/A、--这类占位符的列,会被整体降级成名义型,枚举出几百个取值。

3.2 Excel 与 Matlab 导出的表头陷阱

Excel 的处理方式简单粗暴:打开工作簿,切到需要的那张表,另存为 CSV,弹出提示时确认即可。这里唯一的硬限制是一个 XLS 文件里的多张工作表没法一次性导出,必须一张一张存成独立的 CSV。业务方给过来的文件经常是「汇总」「明细」「说明」三张表放在一起,直接另存只会拿到当前激活的那张,另外两张的数据在后续分析里彻底消失。

Matlab 这边用一行命令就能把矩阵写成 CSV:

% 把工作区里的矩阵写成 CSV,注意这个函数不写表头 csvwrite('bank-data.csv', dataMatrix);

问题就出在这句注释上:csvwrite只写数值,不写属性名。而 WEKA 的 CSVLoader 默认从第一行读属性名,于是第一行数据被当成了列名,整个数据集少一条记录、多五个莫名其妙的属性名。修法有两种,一是在 Matlab 里改用带表头的方式写出,二是老老实实用文本编辑器打开 CSV,在文件最前面手工插一行属性名。

id,age,sex,region,income,married,children,car,save_acct,current_acct,mortgage,pep 1,48,FEMALE,inner_city,17546,NO,1,YES,NO,NO,NO,YES 2,42,MALE,inner_city,27585,YES,2,YES,YES,NO,NO,YES

手工加表头这条看着笨,但可控性最高。注意属性名的个数必须和数据列数完全一致,多一个少一个都会让 CSVLoader 把整行错位。另外属性名里不要出现逗号、引号和空格,中文列名虽然能读,但在后续 Filter 的参数里引用起来很别扭,我一般统一改成英文短名。

3.3 三条路径的取舍与 Explorer 里的兜底转换

把三种来源放在一起对比,选择逻辑就清楚了:

来源推荐做法前置动作最容易出的问题
业务系统 CSVCSVLoader 命令行确认分隔符和编码占位符导致列类型降级
Excel 多工作表逐表另存 CSV每张表单独导出只导出了激活的那张表
Matlab 矩阵csvwrite + 手工补表头补齐属性名行首行数据被当成列名

除了命令行,Explorer 里也能完成转换,而且更适合只想转一两个文件的情况。点Open file选到 CSV,WEKA 会自动调用 CSVLoader 读入,界面右上角的Save按钮另存时把文件类型选成 ARFF 即可。这个路径的好处是转换前能在界面上直接看到属性被识别成了什么类型,发现有一列被误判,当场就能改名或改类型,不用来回切换工具。

注意:另存的时候不要覆盖原始 CSV。转换是有损的,比如数值列被降级成名义型之后,原始精度就回不去了,留一份原件相当于留一颗后悔药。

转换完成只是把数据搬进了 WEKA 的门,真正决定模型质量的是接下来在 Preprocess 面板里做的那些清洗动作。

4. Explorer 预处理实操:删 ID、改类型、Discretize 离散化

Explorer 是 WEKA 里用得最多的模块,绝大多数人一天里九成时间都花在这个界面上。它的布局把功能切成了八个区域,第一次打开容易看花眼,其实每个区域只干一件事。

4.1 八个区域各自管什么

区域位置职责实操要点
1顶部选项卡切换 Preprocess / Classify / Cluster / Associate / Select attributes / Visualize预处理只用第一个,别在其他面板改数据
2选项卡下方按钮排Open / Save / Edit / UndoUndo 是唯一的撤回手段,只能退一步
3Choose 下拉选择 Filter参数在右侧文本框里改
4右侧上方数据集概况(实例数、属性数)转换后先核对这两个数字
5左下列表属性清单,左侧带序号勾选后点 Remove 删除
6右下上方选中属性的摘要看 Type 和 Missing 两个字段
7右下下方直方图,可按类别着色上方下拉框切换着色依据
8最底部状态栏日志与运行状态出错信息只在这里显示

区域 6 里的Type是判断预处理是否成功的关键指标。改完类型之后回到这里看,如果原本显示Numeric的列变成了Nominal,说明改动生效;如果还是Numeric,多半是文件没重新加载,改的是磁盘上的文件但内存里还是旧数据。

区域 7 的直方图在分类任务里特别有用。把着色依据切到类别列,如果某个属性的分布在不同颜色之间几乎完全重叠,这个属性大概率没什么区分度,可以先删掉减小维度;如果某一类在某个区间里高度集中,那这个属性值得保留。下拉框选No Class会变成黑白图,适合看整体分布是否严重偏斜。

4.2 删属性与手改类型

第一步通常是删掉 ID 列。这类列对预测毫无意义,但因为是数值型且取值全不同,某些算法会把它当成强特征,导致模型在训练集上表现很好、换一批数据就崩。在区域 5 勾选id,点Remove,再点区域 2 的Save存一次盘。

删完之后打开 ARFF 文件,会看到 WEKA 已经为每个属性写好了类型声明。这时候有两类改动值得手工做。

第一类是把取值很少的数值列直接改成名义型。以children为例,取值只有 0、1、2、3 四种,本质上是离散的类别,没必要走一遍离散化算法。直接在文本编辑器里改:

% 改动前 @attribute children numeric % 改动后,取值必须和数据段里出现过的完全一致 @attribute children {0,1,2,3}

改完保存,回到 Explorer 重新 Open 这个文件,选中children,区域 6 的 Type 变成Nominal就说明成了。注意数据段里的值不用动,0本身就是一个合法的名义取值。这一步看着简单,但有两个细节:花括号里不能有空格,顺序无所谓但不要重复;如果数据段里有4这个值而声明里没列出来,加载会直接报错,报错信息会指出具体的行号。

第二类是年龄、收入这种连续数值列。有些算法只吃名义型输入,这时候就必须离散化。手工改类型不现实,几百个取值列不完,得靠 Filter。

4.3 Discretize 的三个参数怎么定

在区域 3 点Choose,会弹出一棵 Filter 树,逐级展开到weka.filters.unsupervised.attribute.Discretize,点中它。这时 Choose 右边的文本框会显示当前参数串,点这个文本框会弹出参数编辑窗口。

参数含义默认值什么时候要改
attributeIndices要离散化的属性序号first-last只处理部分列时改成1,4这种列表
bins分箱数量10样本少时降到 3 到 5,否则每箱只有几条记录
useEqualFrequency是否等频分箱否(等宽)数据分布严重偏斜时打开
findOptimalBinNumber是否自动确定箱数否不想手调 bins 时打开,代价是慢

文本框里还可能看到-M之类的开关,不同版本的默认参数串不完全一样,以你界面上实际显示的为准。真正需要动的通常只有attributeIndices和bins两个。

假设这份客户数据里第 1 列是 age、第 4 列是 income,只对这两列做三段离散化:把attributeIndices改成1,4,把bins改成3,其余不动,点 OK 回到主界面,再点 Choose 右边的Apply。区域 5 里这两列前面的图标会从数值标记变成名义标记,区域 6 的 Type 也随之改变。

% 离散化之后,属性声明会变成这种形式 @attribute age {'(-inf-34.333333]','(34.333333-52.666667]','(52.666667-inf)'}

这些区间标签确实晦涩,而且带负无穷符号和方括号,在后续的报告里没法直接讲。想让结果好看一点,可以用文本编辑器做整体替换,把'(-inf-34.333333]'换成0_34这样的短标签。替换时有三点必须注意:单引号要一起替换掉,否则新标签里残留引号会让解析失败;替换顺序从最长的字符串开始,避免短串先匹配把长串切碎;改完之后每一列的名义取值仍然要互不相同,不能出现两个箱都叫low的情况。

如果离散化之后发现分箱不合理,点区域 2 的Undo就能退回上一步。Undo 只能退一步,连续做错两次就得重新 Open 文件,这是 Explorer 设计上的一个硬伤,所以每次做完一步就 Save 一次,用文件版本代替撤销栈。

预处理做完,数据本身干净了,但真正的麻烦往往从这一步才开始——类型改错、过滤器参数没同步、训练集和测试集分布不一致,这些问题在界面上都不会报错,只会在最后的准确率上给你一个漂亮但无意义的数字。

5. 避坑与排查:五条血泪记录

5.1 数据与格式层面的三个翻车点

现象一:加载 ARFF 时报「 nominal value not declared」,指向某一行。原因是数据段里出现了属性声明中没有枚举的值,通常是拼写差异或大小写不一致,比如声明里写YES/NO,数据里混进了Yes。解决方式是在报错行号附近逐个比对取值,把数据统一成声明里的写法,或者把新取值补进花括号。批量修的时候用文本编辑器的「区分大小写」替换,别用一键全替换,否则会把正常值也改掉。

现象二:分类结果的混淆矩阵里出现了一个从没见过的类别。原因是最后一列不是真正的目标变量。ARFF 默认把最后一个声明的属性当 class,如果导出时列顺序变了,或者手工插了一列备注在末尾,WEKA 就会拿备注去做分类,结果自然荒唐。处理办法是在 Preprocess 面板顶部的 class 下拉框里手动指定正确的列,或者调整 ARFF 里属性声明的顺序,把目标列挪到最后。

现象三:CSV 转完 ARFF,某个数值列变成了有几百个取值的名义列。原因是这一列里混进了非数字字符,常见的是空格、千分位逗号、货币符号,或者N/A、-这类占位符。CSVLoader 遇到一个解析不了的値,就把整列降级成名义型。解决方式是回到 CSV 源头,把占位符统一替换成空值(在 ARFF 里对应?),去掉千分位和货币符号,再重新转换。转换后务必看一眼属性声明,不要直接拿去跑算法。

5.2 过滤器与运行层的两个坑

现象四:离散化之后训练集准确率很高,换一份同分布的数据就掉一大截。原因是训练集和测试集各自跑了一遍 Discretize,等宽分箱的边界是根据各自数据的最大值最小值算出来的,两份数据的箱边界根本对不上,同一行数据在训练集里落在第 2 箱,在测试集里可能落在第 3 箱。正确做法是先用训练集拟合过滤器、拿到边界,再把同一套边界套到测试集上。WEKA 的 Filter 支持批处理模式,命令行的写法是先对训练集跑一遍,再用-b加上-r指定测试集,让它复用训练集学到的参数。在 Explorer 里做实验时,更稳妥的方式是只加载一份完整数据,用RemovePercentage或StratifiedRemoveFolds先切分再统一预处理。

现象五:点 Start 之后界面卡住,状态栏的小鸟一直在动,几分钟没反应。多半是内存不够。WEKA 默认堆内存比较保守,数据量上了几十万行、或者选了开销大的算法(比如某些核方法、集成方法),就会长时间 GC。解决方式有两个:一是关掉其他占位大的程序,在状态栏右键手动触发垃圾回收;二是启动时调大堆内存,在启动脚本里给 JVM 加-Xmx参数,比如-Xmx2g。另外,属性维度太高时先做属性选择再训练,比直接加内存更有效。

提示:状态栏是唯一会告诉你「哪里错了」的地方。任何操作没反应,第一件事是看状态栏最后一行,而不是反复点按钮。

这五条里有三条跟格式有关,两条跟流程顺序有关。格式问题靠肉眼核对能解决,流程顺序问题则必须靠规范约束——这也正是下面要讲的,把预处理固化成可复用的配置,而不是每次打开界面凭记忆点一遍。

6. 进阶:把预处理固化成可复用的配置

界面点选的问题在于不可复现。同一份数据,今天点了 Discretize 三段,明天换了台机器点了五段,两次实验的结果就没法比较。真正让 weak 操作入门变成稳定产出的,是把 Preprocess 面板里那一串操作搬到命令行,写成可以反复执行的脚本。

6.1 用命令行串起一串预处理

WEKA 的每个 Filter 都带一个可执行的入口,参数和界面上的完全对应。上面那个「删掉第 1 列、把第 1 和第 4 列离散成 3 段」的流程,可以写成两条命令:

# 第一步:删除 id 列(第 1 列) java -cp weka.jar weka.filters.unsupervised.attribute.Remove \ -R 1 -i bank-data.arff -o bank-noid.arff # 第二步:对剩下的数据里第 1、4 列做 3 段离散化 # 注意删列之后序号会前移,原来第 4 列现在变成第 3 列 java -cp weka.jar weka.filters.unsupervised.attribute.Discretize \ -B 3 -R 1,3 -i bank-noid.arff -o bank-final.arff

-i和-o分别是输入输出,-R是属性序号,-B是箱数。第二条命令里最容易错的是序号:删掉第 1 列之后,所有列的序号整体前移一位,如果还按原来的1,4写,离散化的就是错的那一列,而且不会有任何报错,你只会看到结果莫名其妙地变差。这类错误靠肉眼几乎发现不了,只能在每一步之后打开输出文件核对属性声明。

把这两条命令写进一个 shell 脚本,加上输入文件名的参数,就是一个可重复执行的预处理管线。同一批数据换个月份重跑,只要改一个文件名,其余不动。

6.2 训练集和测试集必须共用同一份过滤器

第 5 章提到的箱边界不一致问题,在命令行里有明确的解法。Filter 的批处理模式用-b开启,配合-r指定要套用的数据集:

# 先用训练集拟合过滤器,-b 表示进入批处理模式 # -r 指定测试集,输出的是套用训练集边界后的测试集 java -cp weka.jar weka.filters.unsupervised.attribute.Discretize \ -B 3 -R 1,3 -b -i train.arff -r test.arff -o test-discretized.arff

关键在-i和-r的分工:-i提供的是「拟合用的数据」,-r提供的是「要被转换的数据」。跑完之后,测试集用的是训练集的箱边界,同一行数据在两份文件里的箱编号一致,模型评估才有意义。这个参数组合是 WEKA 里少数几个不做就会让整套实验失效的地方,值得记牢。

6.3 一次跑通前的三步自检

在把结果拿去汇报之前,我习惯走这三步:

步骤检查内容不通过的表现
一打开预处理后的 ARFF,确认属性数、实例数、class 列数字对不上说明中间某步丢行或丢列
二在 Preprocess 面板重新加载,看每个属性的 Type该是 Nominal 的还是 Numeric,说明离散化没生效
三用十折交叉验证跑一遍基线算法准确率异常高(接近 100%)通常意味着标签泄漏

第三步的十折交叉验证在 Classify 面板的Test options里选Cross-validation,Folds填 10。基线算法选trees.J48就够,它快、结果可解释、对预处理错误也敏感。如果 J48 一上来就是 99% 以上的准确率,先别高兴,回头查有没有把目标列或者与目标高度相关的列留在了特征里。

这套流程我踩过最疼的一次,是在一份几十万行的数据上忘了做第二步核对,离散化因为序号错位作用到了错误的列上,模型在测试集上的表现比随机猜还差。当时排查了两个多小时才定位到那个前移一位的序号。从那以后我每次跑完预处理都强制打开文件核对一遍属性声明,哪怕只改了一行参数也不例外。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进