本节书摘来自华章计算机《数据科学R语言实践:面向计算推理与问题求解的案例研究法》一书中的第1章,第1.3节,作者:[美] 德博拉·诺兰(Deborah Nolan) 邓肯·坦普·朗(Duncan Temple Lang) 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1.3 数据清洗和建立用于分析的数据表示
第一步工作比较简单,创建用于分析的数据结构,赋予变量有意义的名字,并将其转换成合适的数据类型。我们先添加名字如下:
然后,我们将位置、信号、时间变量转换为数值型:
我们也可以将设备类型转换为比数字1和3更容易理解的名字。为此,我们可以为设备类型创建一个多等级的因子,如“adhoc”和“access point”。但是在分析中,对于模型的开发和测试,我们只打算使用在固定接入点测量到的信号强度。这种情况下,需要删除所有与adhoc测量有关的记录,并从数据框中删除type变量。操作如下:
我们共删除了100 000多行。
下面考虑时间变量。按照文档说明,时间是从1970年1月1日午夜开始测量的毫秒数。这源自于POSIXt格式,但POSIXt使用的是秒数,不是毫秒数。因而我们可将时间的值放大为秒数,并为其设置一个time元素类,这样就可以按照R的日期-时间格式进行显示和操作。而原来的精确时间则保存在rawTime变量中,以备今后之需。我们执行如下转换操作:
至此,我们已完成了这些会话。下面检查在数据框中的变量的类型:
并验证它们是否正是我们想要的形式:
这样,数据有了正确的形式以及正确的类型。下面验证数据的实际值是否也是合理的。这里,我们可以采用很多种方法。首先,查看每个数值型变量的汇总值:
我们也将字符型变量转换为因子型,并作检查:
在检查完这些汇总之后,我们发现存在几处异常:
scanMac只有一个值,这是进行测量的手持设备的MAC地址。可以从数据框中去掉这个值。但我们也可能需要有这个值,以便将其与在线数据比较。
posZ的所有值为0,它是手持设备的相对高度。这是因为所有的测量是在大楼的同一层进行。我们也可以去掉这个变量。
因此,我们对数据框做如下修改:
1.3.1 对于方向数据的探索
按照文档说明,方向可以取8个值,即0,45,90,…,315。我们对其进行检查:
显然,这不符合实际情况。让我们检查一下方向值的分布:
图1-2给出了这个图的加标注版本。它表明方向值在预期角度附近以聚类方式分布。注意,接近于0和接近于360的值都表示相同方向。即在比0°少1°的方向被报告为359°,而多1°的方向则为1°。
图1-2 手持设备方向的经验CDF。这个关于方向的经验分布函数表明存在8个按照45°分隔的方向。从函数的台阶上可看出,这些方向并不是精准的45°、90°、135°等。此外,0°方向被分成两个组,一个组在0°附近,另一个组在360°附近
虽然将实验设计成按照8个方向(按照45°的间隔,从0°到315°)测量信号强度,但这些方向不是精准的。不过,这种8等分空间的角度值处理方式将有助于我们对问题的分析。即我们把47.5°映射为45°,358.2°映射为0°,等等。为此,我们取出每个方向值,找出它与8个方向中的哪一个最接近,然后返回这个方向。在处理类似于358.2这样的值时要特别小心,我们要把它映射为0,而不是最接近的315。下面的函数完成这种转换:
我们使用roundOrientation()创建近似角度,并且,我们再次保留原始变量,在数据框中增加新的角度值。
我们用箱线图检查该结果是否与预期的相同:
从图1-3我们看到新的值均为正确的,360°附近的原始值被映射成0。它也显示了在测量操作中的易变性。
图1-3 关于手持设备方向的箱线图。从这个原始方向与近似值相对照的箱线图,可以确认这些值被正确地映射到0°、45°、90°、135°等。左上角的“异常值”是360°附近的值被映射到0°
1.3.2 对于MAC地址数据的探索
从summary()函数的信息中可看出,在接入点和频道的MAC地址之间似乎存在一对一映射关系。例如,汇总统计值表明地址00:14:bf:3b:c7:c6具有126 529个实例,而频道2432000000也有相同个数的实例。为了进一步确定是否存在一对一映射关系,我们查看MAC地址和频道之间的关系。
究竟有多少个唯一的地址和多少个唯一的频道?如果存在一对一映射关系,它们应该具有相同的个数。我们发现:
存在12个MAC地址,8个频道。从大楼平面图(见图1-1)得到的印象是只有6个接入点。为什么有8个频道和12个MAC地址?再仔细阅读文档,我们发现在不属于测试区域的地方还有接入点,而在平面图上看不到它们。使用table()函数可检查各个MAC地址的观测值的计数:
很清楚,第一个和最后两个MAC地址不属于测试区域,因为它们的计数值非常小。它们只是在测试过程中工作或激活了一小段时间。第三个和第五个可能也不属于地图上显示出来的接入点,因为它们比其他接入点的计数小得多,远低于可能的146 080个记录(前文已说明该数据为可记录的潜在信号个数,即166个网格点×8个方向×110次重复检测)。
按照文档说明,接入点由5个Linksys/Cisco路由器和1个Lancom L-54g路由器组成。我们在网站http://coffer.com/mac_find/上查找它们的MAC地址,发现以00:14:bf开头的厂商地址属于Linksys的设备,以00:0f:a3开头的地址属于Alpha Networks公司,而Lancom公司的设备地址以00:a0:57开头(见图1-4)。我们的确有5个地址以00:14:bf开头的设备,正好与文档中说明的Linksys的个数匹配。可是,没有一个MAC地址以00:a0:57开始,这不符合文档说明。但不管怎样,我们发现了有价值的信息,将它们整合起来可更好地理解数据。下面,我们只保存排序前7位的设备的数据记录。执行如下命令:
图1-4 网页coffer.com Mac Address Lookup Form的截屏。coffer.com网站提供从厂商名到MAC地址的查找服务,以及从MAC地址到厂商名的查找服务
最后,我们为余下的“MAC×频道”组合建立计数表格,确认在每一行中有一个非零入口项。
对于这7种设备,我们确实发现存在MAC地址和频道之间的一对一关系。这意味着我们可以从Offline中删除channel变量,即执行:
1.3.3 对于手持设备位置数据的探索
最后,我们考虑位置变量posX和posY。我们到底在多少个位置上采集过数据?对应于每个唯一的(x,y)组合,by()函数可以统计数据框中所对应的行的个数。我们先创建一个数据框列表,其中的每个数据框对应一个位置。
注意,这个列表的长度大于具有测量记录值的实际的(x,y)位置组合的个数。其中的许多元素是空的:
这些空值对应于没有观测到的(x,y)组合。我们删除这些无用的元素,
并确认我们只有166个位置:
我们可以对每个数据框进行操作。例如,确定在该位置上所记录的观测值的个数。
并且,如果想要保存该位置的位置信息,可执行如下操作:
执行下面命令,我们确认locCounts是一个3行矩阵:
下面我们检查几个计数:
我们看到每个位置上大概有5500个记录。这与8个方向×110次重复×7个接入点(6160个信号强度测量值)是一致的。
我们可以通过将计数作为对应位置上的文字说明,对166个观测位置进行可视化显示。为了避免图中的文字重叠,需要改变字符的大小和角度。我们先对矩阵进行转置,使得位置作为矩阵的列。然后进行绘图:
从图1-5可知在每个位置上检测到的信号个数大致相同。
图1-5 在每个位置上检测到的信号的计数。在大楼的每个位置上绘制的数字,对应于在离线数据中从所有接入点检测到的信号的总数。理想情况下,在每个位置上,对应于6个接入点,每个接入点具有8个角度,在每个角度上测量到110个信号,共计为5280个记录值。数据中虽然还包含第7个MAC地址,但不是所有的信号都被检测到,因此,每个位置上大约有5500个记录值
1.3.4 数据准备函数的创建
我们已经检查了除time和signal之外的所有变量。这个处理有助于我们清洗数据,并将数据缩减成只与分析相关的记录。我们将针对信号的检查留到下一节,在那里研究它的分布特性。至于time这个变量,它说明进行观测的次序,与我们的模型并没有直接关系。但对于实验,它有助于发现潜在数据源的偏差。例如,携带手持设备的人,在实验进行过程中,可能会改变设备的携带方式。这个变化可能会导致信号强度的变化。将时间和其他变量之间的关系进行绘图并做分析,有助于发现这种潜在问题。我们将这个调研留作练习题。
由于我们要把在线数据读入R,因此需要将所有这些读命令放进一个称为readData()的函数中。还有一个原因是,假如今后我们改变了主意,想要处理一些特殊情况,例如,保留channel或posZ,则只需对函数做简单修改并重新运行。我们甚至可以在函数定义中增加一个参数,以允许我们按其他不同的方式处理数据。我们将readData()函数的创建留作练习题。
我们调用readData()来创建离线数据框如下:
然后,我们使用identical()函数,对照已经创建的数据框,检查这个数据框的新版本:
这个结果确认了我们的函数是按照预期结果执行的。
当我们把代码汇集到一个函数里时,常常会忘掉所需要的一些变量。这些代码却还能运行正确,是因为它们是在当前R会话(即globalenv())中建立的。但在新的R会话中,我们可能会以其他不同方式定义这些全局变量,这样该函数将不能正确运行,或者得出错误结果。我们可以使用codetools程序包[10]中的findGlobals()函数,识别出哪些是全局变量,即
processLine()函数是一个变量,由于它在readData()对lapply()的调用中作为参数,这不会有问题。subMacs也被识别为全局变量。这个变量是在全局环境中,根据mac的唯一值来创建的(见1.3.2节),但我们在函数中忘记了将该部分代码包含进来。因此,我们要么修改函数将subMacs作为一个带有合适缺省值的参数传递进来,要么在函数中创建subMacs,但这样的话,subMacs就不再是全局变量了。