大多数浏览器和
Developer App 均支持流媒体播放。
-
使用 TabularData 在 Swift 中探索和处理数据
探索如何使用 TabularData 框架在 Swift 中加载、探索和处理非结构化数据。无论您是需要为机器学习任务预处理数据,还是需要在 app 中实时生成数据摘要,这个框架都会对您有所帮助。了解这个框架如何帮助您处理大型数据集、将多个数据表格联接在一起,以及通过编程方式筛选数据。此外,我们还将向您展示如何在您的 app 中使用 DataFrame 来推动实现所有以数据为中心的功能。
资源
相关视频
WWDC22
-
下载
Alejandro Isaza:大家好 我是 Alejandro David 和我今天将为大家 介绍 TabularData 这是一种用于操作和 探索数据的全新框架 我们先从快速简介开始 然后谈谈数据浏览 数据转换和最佳做法 最后做一下总结 我们直接开始吧 首先来谈谈 什么是 TabularData 用最简单的术语 TabularData 就是 整理成行与列的数据 这与电子表格类似 但请试想一下 如果数据有数百列 几百万行 这就是 TabularData 框架的用武之地了 它究竟是什么? 它是我们一直 在研究的全新框架 如今已经在 macOS、iOS Apple tvOS 和 watchOS 中可用 可以帮助您探索和 操作非结构化数据 说到“非结构化数据” 我指的是没有按照 预定义方式排列的数据 举个例子 如果您下载 一个没有技术规格的数据集 如天气数据或人口统计数据 在遇到新数据集时 您要做的第一件事 就是探索这个数据集 您要了解里面有什么信息 例如 值是什么? 类型是什么?数据是如何表示的? 有没有缺失的值?诸如此类 您需要能够回答这些问题 从而正确地解读数据集 并且能够继续下一步 也就是操作数据 操作数据指的是 根据您尝试解决的问题 将数据集转换为 某种最合适的形式 例如 在表示日期时您可能要 使用日期类型而不是字符串 或者 您可能要 将 x 和 y 坐标 组合成点类型 诸如此类 TabularData 框架 非常适合处理大型数据集 这里有些常见的用例 按照某一标准对数据分组 比如按年龄对人员分组 基于共同值连接数据集 比如将交易表格 与买家信息连接在一起 将数据拆分或分段以逐步处理 或筛选为整个数据集的一个子集 还有构建数据管道 例如为机器学习进行功能设计 在框架的语境中 表格称为 DataFrame DataFrame 包含行和列 这与电子表格类似 但与电子表格不同的是 每个列只能包含一种类型的值 但这也意味着 列可以容纳任何类型 甚至是您自定的类型 如字典、GPS 坐标 或原始音频采样 每当我们显示代表 DataFrame 的表格 或 DataFrame 切片时 我们会在左侧包含一个索引列 若要按照数据索引访问行 则这么做是有意义的 一些操作 如筛选等 不会改变数据索引 而另一些操作 如排序等 则可能会改变 生成的 DataFrame 的索引 我们来进行分解 放大到一个列上 正如我在前面提到的 列具有特定的元素类型 这个示例中为整数 它还具有一个名称 必须在 DataFrame 中是唯一的 列用 Column 类型表示 也就是集合 就跟数组一样 可以用名称来引用列 但许多情形中也需要使用类型 有一个结构称为 ColumnID 用来存放列的名称和类型 也可用于引用具体的列 建议您尽可能使用 预定义的 ColumnID 而不是字符串字面值来引用列 还要注意一点 DataFrame 中的所有列 必须具有相同数量的元素 但也总会有缺失的元素 用空值来表示 与 Column 类似 我们有 Row 类型 您可以通过列名称或索引 来访问行的各个元素 您可以把 Row 想成代理 实际上不包含行中的元素 而是指回到 DataFrame 中 某个行的引用 如何创建 DataFrame? 我来给您演示一下 可以从字典字面值 或者通过逐一构建各个列 来创建 DataFrame 这是从字典字面值构建的示例 注意在使用字典字面值时 您只能使用 基本的 Swift 类型 如字符串、数字、布尔值和日期 另请注意 每个列 必须具有相同数量的元素 一种更常见的 DataFrame 构建方式是 逐一构建每个列 然后将各个列 追加到 DataFrame 这里举个例子 先创建一个空 DataFrame 再创建一个列 然后将这个列 追加至 DataFrame 我可以对所有列重复这个过程 再提醒一下 请确保每一个列 具有相同数量的元素 并使用唯一的列名称 您现在已知道 DataFrame 是什么了 我们来做些数据探索 首先要做的是载入数据集 TabularData 支持读取 逗号分隔值 即 CSV 以及 JSON 文件 只需通过文件 URL 调用构造器 很简单 它会使用所有默认选项载入 我们来探索在载入 CSV 时 可用的一些选项 如果您以前使用过 CSV 可能就会知道 逗号不一定总会存在 分隔符可能是制表符或分号 可能存在或不存在 含有列名称的标题行 其他变化包括字符串 对特殊字符的转义方式 以及缺失值的表示方式 TabularData 可以应对 所有这些变化 在这个示例中 我指定的是没有标题行 使用自定 nil 编码忽略空行 并且使用分号分隔符 请参阅相关文档 来了解一组完整选项和默认值 如果您的文件较大 您可能想要 一次只载入行的一个子集 这可通过行选项来实现 例如 这样做只会 载入前 100 行 类似地 您可以选择 列的一个子集 为此 可以使用 columns 参数 注意这也允许您 对列进行重新排列
我来简略说说 载入 CSV 文件时 类型推理的运作方式 CSV 文件全部基于文本 但若每个列都是字符串类型 这会不太方便 因此在载入 CSV 文件时 TabularData 会先尝试 将值转换为数字、布尔值和日期 然后再默认为字符串 如果要将值强制为某一种类型 或者想要让载入速度变快一些 您可以明确指定列的类型 这可通过 types 参数来实现 在这个示例中 我们将 id 列 指定为整数类型 name 列指定为字符串类型 这不仅能加快载入过程 而且在遇到一个值无法 转换为指定类型时 也可以抛出错误 这种做法非常好 因为它能让您轻松捕捉问题 并进行适当处理 而不会最终发现 列的类型与您预期不符 那会造成以后 app 发生崩溃 最后来谈谈日期解析 TabularData 默认 使用 ISO8601 格式 来检测和解析日期 如果您的 CSV 文件包含 采用其他格式的日期 您需要指定一种 自定日期解析策略 后面我们进行演示时 David 会给大家讲解这一操作 并举例说明 现在 我们换个话题 谈一谈如何写出数据 第一种最简单的选择是 使用 Swift 的 print 函数 这会在“终端”中生成 一个排列整齐的表格 打印输出包括行索引 列名称、列类型 前几行数据 以及行数和列数 还会指出屏幕中 无法显示所有的行 也无法显示所有的列 这个示例中 还有 10 行未显示出来 print 非常适合探索和调试 但显然不适合用来存储数据 如果要将 DataFrame 保存为 CSV 文件 可使用 writeCSV 方法 有一点务必要注意 writeCSV 会使用各个值的 默认字符串转换方式 在列中使用自定类型需要留心 因为生成的 CSV 可能是 无法被读回的那种 一般的原则是 在您的列中仅使用 基本的 Swift 类型 在写入到 CSV 时 可能需要对部分列进行转换 writeCSV 具有一些 与读取选项类似的选项 可用来自定 CSV 数据写入方式 举个例子 在停用标题的地方 我使用了自定 nil 编码 和自定的分隔符 如果要访问特定的行 可以仅使用 row 子脚本 然后访问该行的某一特定列 但您应该尽可能 先访问列 再访问行 您可以这样来访问列 在按 Name 访问列时 您可以在子脚本中 省略 column: 标签 也可以访问列的子集 这里您得到的是 一个 DataFrame 切片 DataFrame 切片与 DataFrame 非常相似 基本上就是对原始 DataFrame 的引用 在大多数情形中 您甚至不需要知道 它是完整的 DataFrame 还是一个切片 最后 也可以选择列的子集 这会返回一个新 DataFrame 其中仅包含选择的列 还可以通过 filter 方法 来选择行的子集 filter 运算的结果是 一个 DataFrame 切片 与选择一个行范围相似 但与行范围不同的是 filter 可以返回不连续的行 您需要谨慎处理 DataFrame 切片索引 与数组切片类似 其索引反映的是原始行的索引 具体而言 第一个索引可能不是零 下一个索引可能不是 当前索引加一 对于字符串索引 您需要使用 startIndex 而非零 endIndex 而非计数 以及 index(after:) 而不是加上一 我已阐述了基本概念 现在通过构建一个 app 来付诸实践 在旧金山寻找停车位是件难事 David 和我想要构建 一个 iPhone app 显示街道上附近的停车位 我们要使用城市发布的数据 标识附近当前允许停车的 停车计时器 我们知道有一个数据集 但不清楚它的确切内容 所以 第一步是探索数据集 以了解我们拥有的信息 现在请 David 来讲下面的内容 David Findlay:谢谢 Alejandro 大家好我叫 David 是一名框架工程师 在这个演示中 我将通过一个示例 演示如何使用 TabularData 来探索数据集 首先是探索一个包含 停车政策的 CSV 文件 第一步是载入数据 这做起来很容易 只需将文件 URL 传递 到 DataFrame 构造器中 注意构造器是可抛出的 这在处理潜在的解析 错误时很有用处 接下来 通过简单的 print 我可以探索前面几行和几列
载入需要几秒钟 这是因为 DataFrame 载入到内存中的数据 超过一百万行和 15 列 第一次探索数据集时 通常不需要整个数据集 所以 我在载入数据时 通过指定行范围来 加快探索速度
接下来我将查看这些列 注意右侧隐藏了两列 因为屏幕中无法显示它们 我来演示如何解决这个问题 可以使用 formattingOptions
通过 formattingOptions 我可以配置数据呈现方式 在这个示例中 我将 maximumLineWidth 增加到 250 将列宽度减小到 15 并将行数减少到 5 以避免滚动显示打印结果 然后 只需使用 description 方法 将 formattingOptions 添加到 print 语句中
太棒了!现在可以探索所有列了 我选择保留几个有趣的列 这也是重新整理列的好机会 将它们按照我想要的顺序列出 我保留的列有 HourlyRate 和 DayOfWeek startTime 和 endTtime StartDate 和 PostID 接下来我只需要 在载入 DataFrame 时 将这些列作为参数添加
好了 这样探索起来 已经简单多了 看一看 StartDate 列 其类型为字符串 这是因为只有 ISO8601 日期 才能自动检测 任何其他日期格式 都需要明确指定 要解决这个问题 可以使用 CSVReadingOptions Alejandro 之前已解释过 通过使用 Foundation Date Parsing API 添加一个日期解析策略 指定年月日的格式 语言区设为美国英语 时区设为太平洋标准时间 然后在载入 DataFrame 时 传递 CSVReadingOptions
StartDate 列现在有 正确的类型了 我可以轻松筛选 DataFrame 使它仅包含有效的停车政策 从代表当前日期的变量开始 使用 filter 方法对 DataFrame 进行筛选 filter 方法会取列名称 本例中为 StartDate 以及类型 Date 在括号中 unwrap 可选的 date 在 date 值为 nil 时 返回 false 使它不出现在筛选结果中 最后 我会保留小于或 等于当前日期的开始日期 接着我来更改 print 显示筛选后的结果
从现在开始 我不再需要 StartDate 列了 所以我来把它移除掉 但我得小心一些 因为无法从 DataFrame 切片中移除列 必须先转换为 DataFrame 并让 filteredPolicies 成为 var 的对象 因为移除列是一个突变方法 现在可以移除列了 使用 removeColumn 方法 并将 StartDate 列 指定为要移除的列 好了 这是停车政策数据集中 我要探索的全部内容 在下一个部分中 Alejandro 将会探讨 如何来增强您的表格数据 交回给您 Alejandro Alejandro:谢谢 David 现在 我对数据集有了 一些不错的了解 下一步是进行转换和增强 以满足我们的需求 最简单的转换类型是 更改列中的值 这可以采取 map 运算的形式 将各个值映射为新值 或许是不同类型的新值 为方便操作 TabularData 就地提供了 一个 map 版本: transformColumn 在这个例子中 我要将 DayOfWeek 列 从字符串转换为整数 用来代表星期几 代码看起来会像这样 对于每个元素 我们将字符串转换为整数
与 transformColumn 类似 decode 方法处理数据的解码 在操作 CSV 文件时 您可能会遇到数组或字典 作为 JSON 值 嵌入在 CSV 中 TabularData 为此 提供了 decode 方法 这里有个例子 其左侧的 DataFrame 含有嵌入的 JSON 数据块 通过 decode 您可以使用 JSONDecoder 将列转换为自己的类型 示例中为 Preferences 代码看起来会像这样 注意 Preferences 类型 需要遵从 Decodable 协议 并且列需要包含 类型为 Data 的元素 这是 JSONDecoder 预期的输入 另一种实用的运算 是 filled 方法 它可以让您将列中所有 缺失的值替换为默认值 在列运算列表的末尾 我要提一下 summary summary 可为您提供 列内容的简短概要 summary 方法返回分类摘要 包含元素的数量 显示在 someCount 的描述中 缺失元素的数量 显示为 noneCount 唯一元素的数量 以及最常出现的值 也就是 mode 也有数值摘要 它仅适用于 含有数字值的列 它也包括计数 还有平均值、标准差 以及其他统计数据 现在我来显示一个 summary 打印结果 但您也可以直接 使用 summary 结构来访问统计数据 例如 如果您要筛选出 75% 百分位中的分数 那会涉及许多列转换 但列转换不是最有趣的 DataFrame 转换 才是真正让人感兴趣的 与列转换不同 DataFrame 转换 会同时操作多个列 一个简单的例子就是排序 我们都知道排序的工作方式 但我还是通过演示来阐明一下 我们按照分数对这个表格排序 这会作用于所有的列 另外还要注意 行索引会在排序时改变 另一种有趣的 DataFrame 转换 是 combineColumns 通过 combineColumns 方法 您可以将多个列合并为一个 例如 假设您有不同的列 来表示纬度和经度 但您想将它们合并为 CLLocation 类型 您可以按照这个例子来操作 首先 我会指定要合并的列 为新列取一个名称 接着指定输入列 和新列的类型 注意一切都必须是可选的 我需要处理缺失值的情况 再构建新值 与列类似 也有一种适用于 DataFrame 的 summary 方法 它会返回每个列的摘要统计 注意如果 DataFrame 很大 它的成本会很高 也许最好是仅对您 感兴趣的列进行概括 另一种有趣的方法是 explode 此方法会取包含一个元素数组的列 为数组中的每个元素创建新列 我们来看这个示例 这一次 Scores 列中 包含了嵌入的数组 代表每个人的分数 如果将 explode 运算 应用于 DataFrame 每个分数会成为一个新行 具有多个分数的人 的名字会重复出现 这对筛选很有用处 也可用于进行需要研究 个别分数的其他运算 手头有了这些工具后 现在我将话题交回给 David 他会帮助我们将计时器数据 转换为我们需要的形式 David 给我们演示一些代码吧 David:谢谢 Alejandro 虽然不知道您的情况 但对我来说 停车最重要的一点是位置 幸运的是 另一个 CSV 文件 正好是我所需要的 我来演示里面有些什么 与之前一样 首先载入数据 不过这一次 我已知道 哪些列是我感兴趣的 POST_ID、STREET_NAME STREET_NUM、LATITUDE 和 LONGITUDE 这些列 与上一演示中一样 我将使用 formattingOptions 来打印结果 我想要的第一个增强 是将纬度和经度列 合并成一个新列 类型为 CoreLocation combineColumns 方法 完美适合这项工作 这里 我将纬度和经度列 合并成一个新列 命名为 location 在括号中 我指定了 latitude 和 longitude 类型参数以及 CoreLocation 返回类型 接下来 unwrap 可选的 纬度和经度值 在任一值为空时返回 nil 最后 将纬度和经度值传递 给 CoreLocation 构造器 DataFrame 中有了位置数据 就能开始构建 app 的第一个功能 根据给定的位置搜索 最近的停车计时器 我将写一个函数 命名为 closestParking 它会取位置、DataFrame 和停车计时器数量 包含在搜索结果中 我从本地副本开始 再使用 Alejandro 在前面 介绍的 transformColumn 方法 将位置转换为距离 然后 当然还要将 location 列重命名为 distance 最后对 distance 列升序排序 以限制返回的停车位数量 为了增加点乐趣 我们用旧金山的 Apple Store 商店做个测试 插入我在 Apple 地图上 找到的坐标 和 meters DataFrame 并将搜索结果限制为五个停车位 非常好!看起来 Post 街道的 Apple Store 商店附近 有许多停车位 app 的第一项功能表现不错 但如果最近的所有停车位 都已经被占用该怎么办 app 的下一项功能 是寻找停车位最多的街道 但在实现这项功能前 我先介绍称为“分组”的新概念 分组是将数据拆分成多个组 给定一个分组列 例如 STREET_NAME 列 group 方法首先找到 唯一的街道名称值 Post 街道、California 街道 和 Mission 街道 再将行拆分到对应的组中 每个组都是一个 DataFrame 切片 我们回到代码中 使用 grouped 方法 按街道名称对 meters 分组 然后统计每个街道组 有多少个停车计时器 并按降序方式显示结果 停车位最多的街道 显示在结果的最上方 这正是我的 app 所需要做的 简直太棒了! app 有两个强大功能了 稍等一下 我刚发现第一个功能有个错误 最近的停车计时器只是 考虑了 meters DataFrame 而其实我需要的是 停车政策有效 且距离最近的停车计时器 这变得有趣了 因为该信息在演示一的数据中 我来演示如何连接 两个不同来源的数据 从而解决这个错误 如果您之前使用过关系数据库 或许会熟悉连接的概念 它可以让您使用一个键 将两个 DataFrame 合并到一起 这个键是同时存在于这两个 DataFrames 中的某个值 在 meters 和 policies 这两个 DataFrame 中 这个键是 POST_ID 用于对停车计时器进行唯一标识 join 运算生成一个 DataFrame 在其包含的行中 来自 meters 的 POST_ID 与来自 policies 的 POST_ID 相匹配 构成这些行的数据是 来自左侧 DataFrame 和 右侧 DataFrame 的匹配数据 注意列名称 具有前缀 left 或 right 这表示列来自于 join 运算的哪一侧 前缀有助于避免连接 结果中出现命名冲突 这个运算是内连接 是默认连接方式 还有三种连接方式 左外、右外和全外连接 这里就不细讲了 详细信息请参考相关文档 对于 TabularData 增强 我要讲的就是这些 在下一部分中 Alejandro 将会 谈谈最佳做法 Alejandro:谢谢 David 我们现在有了所有的数据 而且已转换成我们需要的形式 是时候构建 app 了 我来谈一谈如何 重复利用探索代码 同时准备好用于生产环境 回到我们一开始用来 载入 CSV 文件的代码 如果您这样做 这些列就会有未知类型 这会给 filter 或 join 等 运算造成问题 因为那时您需要提前知道类型 如果从用户提供的来源载入数据 对类型进行猜测是有风险的 可能会导致您的 app 崩溃 相反 您应该在载入数据时 声明您预期的类型 就像这个示例中一样 这里 我为关注的每个列 定义了 ColumnID 然后同时将列名称和 列类型提供给 CSV 构造器 记住 不论是在哪个方法中 您都可以使用 columnID 而不是字符串来引用列 现在 如果存在无效的值 您将得到一个异常 您可以进行处理 比如向用户显示错误 这样您就能确保拥有 您预期的列和列类型 这在使用自定日期格式时 尤其重要 因为如果您不将 列的类型指定为 Date 日期解析可能会 以静默方式出错 并生成一个字符串列 将它强制为日期类型 可以抛出异常 包含出错的单元格的内容 这有助于您对问题进行调试 说到错误 载入 CSV 文件时 您可能会遇到这几类错误 在使用自定日期解析器 并且单元格解析失败时 会发生解析失败错误 其他几个错误不言自明 具体请参考相关的文档 最后 我简要说一下性能 许多时候 您应该不必担心性能 但在一些情形中 在处理大型数据集时 您会看到较大的影响 第一个是载入 CSV 时的 解析日期 日期解析具有许多 特殊情形和注意事项 因此速度往往较慢 如果载入 CSV 文件 用时超过了几秒钟 这是您要寻求做出改进 的第一个地方 一个方案是推迟解析 如果您不是马上需要日期信息 此方案的效果特别好 比如您可以首先进行 筛选或分组 如果这个方案不可行 可以考虑制作一个日期解析器 针对您的日期字符串 进行性能优化 在分组时务必使用 含有基本 Swift 类型 的列来作为分组列 例如字符串或整数 这可以加快分组性能 如果要对多个列进行分组 不妨先将这些列合并成 一个使用简单类型的列 然后再进行分组 例如 您可能要按照 星期几和计时器类型来分组 可以考虑将这两个属性 合并成一个字符串 例如 day-type 类似地 在连接数据时 可以考虑对含有基本 Swift 类型的列进行合并 到这里 我们已准备好 完成这个 app 了 David 我们来总结一下吧 David:使用 TabularData 的最佳做法 我要编写 app 的搜索功能 Parking 结构将存储 连接的 meters 和 policies DataFrame 我也定义了一个 location ColumnID 因为多个方法都需要它 我们来仔细看一看 loadMeters 方法 最上面是 Column ID 载入 meters 时会用到 接着载入 meters 并指定各个列的预期类型 如果提供的 CSV 文件中 有任何不匹配情况 这会抛出错误 接下来我们来 验证解析的列 是否完全符合我的预期 否则抛出自定的 ParkingError 最后 我重新构造了 combineColumns 运算 以使用 latitude、longitude 和 location column ID 这样 app 的搜索功能 就已准备好投入生产了 现在交回给 Alejandro 对 TabularData 框架 做一个总结 Alejandro:谢谢 David 我们来总结一下 我们今天演示了 如何使用 TabularData 探索未知数据集 对其进行操作 并将它引入到 app 中 我们探索了一个数据集 研究了一些列和数据转换 并在最后围绕错误处理 和性能介绍了一些最佳做法 我迫不及待想要看看 大家是如何使用 TabularData 来制作优秀 app 的 谢谢!
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。