SAP-数据科学实践指南-全-

SAP 数据科学实践指南(全)

原文:zh.annas-archive.org/md5/c8698e0ee1bd664111435c9077e3ad74

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

数据科学和人工智能的未来前景前所未有地光明。AI 现在能在从反射神经动作的乒乓球到深思熟虑的围棋等各种游戏中击败人类。深度学*模型几乎可以像人类一样识别物体。有些人甚至说自动驾驶汽车的表现比他们分心的人类对手更好。过去十年中,数据量的巨大增长、存储容量和计算能力的提升,促使了数据科学的快速进步。

当然,技术已经扩展到您业务的每个方面(从财务和销售到生产和物流)。然而,您的业务每个部分是否都被数据科学和人工智能所推动?很可能不是。尽管它们可能非常美妙,但如果您不是在设计自动驾驶汽车或者预测客户行为,您可能并未使用这些技术。

许多组织可能可以访问企业资源规划(ERP)系统(如 SAP)中的业务数据,您的情况可能并无不同。来自诸如 SAP 之类的业务系统的数据通常是完美的,因为通常在允许保存到数据库之前已经进行了验证和检查(而数据科学家最基本且回报最少的任务之一就是清理数据)。这意味着 SAP 中的 ERP 数据可以被轻易获取,而数据科学正是用来进行这种收割的!

让我们来看一个假设的场景。大宝矿业仓库的 SAP 团队处于持续改进的状态。他们知道如何配置他们的 SAP 系统以完成用户要求的任务,并且他们精通这个系统,忠实地接受请求并提供解决方案。然而,在报告和分析方面存在一些问题;他们拥有数据仓库和商业智能系统,但是开发报告却是一个需要数月时间的过程。团队经常使用标准的 ALV(ABAP 列表查看器)报告,这些报告的功能非常有限,因为它们需要开发人员编码;此外,很难利用与 SAP 结合使用的大量公共数据。就像无数其他企业一样,大宝矿业仓库内的 SAP 数据是一个孤立的岛屿,与其系统内部隔离开来。不与 SAP 合作的团队不知道其中的内容,而与其合作的团队花费大量时间维护系统,没有机会向外看。

然而,SAP 数据不应该成为一个孤立的岛屿。团队了解他们的数据,知道如何找到它,以及他们希望如何利用它。然而,当涉及到分析数据时,每个人都被那些需数月开发的报告流程所束缚。

听起来耳熟能详吗?这几乎是我们曾经与之合作过的每家 SAP 店铺的故事。在我们三十多年的共同经验中,这样的店铺不计其数。

我们希望为那些 SAP 团队(包括你的团队!)提供一些现代见解——可以在不定义数据立方体、数据仓库对象或学*复杂前端报告的情况下使用的工具和技术。在本书中,我们将呈现简单的场景,例如将数据直接从 SAP 导出到平面文件,以及导入报告工具中。这对于临时报告和调查非常有用。我们还将考虑更复杂的场景,包括在云中使用提取工具和神经网络模型来分析数据,这是在 SAP 或当代数据仓库中不可能实现的方式。

如何阅读本书

您需要从概念角度来阅读本书。我们提出了分析业务数据的替代技术。我们要求——不,我们恳求读者以新颖有趣的方式思考业务数据(特别是 SAP 数据)。本书旨在唤起围绕如何弥合特定业务数据与数据科学进展之间差距的想法。您不需要成为计算神经网络中梯度下降的复杂算法的专家,也不必成为您的业务数据的专家。但是您确实需要有渴望跨越这两个阵营并在此过程中享受乐趣的愿望。¹

从数据科学家的角度来看,本书中的数据科学原理是一个介绍。如果您能在五十步之外识别出 sigmoid、tanh 或 relu 激活函数,您可以跳过这些部分。但是我们打赌,如果您在数据科学方面的大师级水平如此之高,那么在 SAP 方面您可能是个新手。集中精力关注 SAP 故事,向您展示如何提取事物并演示如何使用系统中的真实业务数据。

从 SAP 专业人士的角度来看,您将突破传统的报告和分析模型。您将学会以机器学*和深度学*术语思考业务应用和报告。这听起来可能有些神秘,但是在读完本书时,您将拥有必要的工具来迈出这一步。在此过程中,您将自动检测销售数据中的异常,从过去数据预测未来,将文本处理为自然语言,将客户分成智能群组,以及精彩地可视化所有这些内容,并教导机器人使用业务数据。

在我们的 AI 和数据科学世界中,询问数据相同的老问题是陈旧的、幼稚的,而且(坦率地说)令人厌倦。我们希望您提出一些您甚至不知道自己可以提出的数据问题。也许中国的茶叶价格确实对您的销售产生了异常的影响。

从开发者的角度来看,你将被激发学*像 Python 和 R 这样的精彩编程语言。我们不会教你这些语言,但我们挑战你尝试涉足这些温暖而活泼的水域。如果你已经是经验丰富的 R 或 Python 开发者,你对代码部分有良好的掌握。对于新手,我们会指引你找到入门资源。如果你倾向于使用其他语言如 Java,也不必感到被忽视。本书的“元”目标是让你思考如何以不同的方式处理业务数据,如果这意味着你想使用 Java,尽管去做吧。

数据科学的运作是一本书的内容。我们经常会涉及如何实现我们提出的想法,但是深入探讨创建健壮管道不在本书的讨论范围之内。

提示

数据科学家可以跳过第二章。SAP 专业人员可以跳过第三章。我们在本书后面讲述的故事将融合这两个学科,所以我们希望来自其中一方的读者能够公平地理解我们将如何研究我们的魔法。

本书中使用的约定

本书使用以下排版约定:

斜体

表示新术语、URL、电子邮件地址、文件名和文件扩展名。

常量宽度

用于程序清单,以及段落中引用程序元素,如变量或函数名称、数据库、数据类型、环境变量、语句和关键字。

常量宽度粗体

显示用户应该逐字输入的命令或其他文本。

常量宽度斜体

显示应由用户提供值或由上下文确定的值替换的文本。

提示

这个元素表示提示或建议。

注意

这个元素表示一般注意事项。

警告

这个元素表示警告或注意。

使用代码示例

本书的目的是帮助你完成工作。通常情况下,如果本书提供了示例代码,你可以在你的程序和文档中使用它。除非你要复制大部分代码,否则不需要我们的许可。例如,编写一个使用本书多个代码片段的程序不需要许可。出售或分发包含 O'Reilly 图书示例的 CD-ROM 需要许可。通过引用本书回答问题并引用示例代码不需要许可。将本书的大量示例代码整合到产品文档中需要许可。

我们感谢,但不要求,署名。署名通常包括标题、作者、出版商和 ISBN。例如:“《实用 SAP 数据科学》由 Greg Foss 和 Paul Modderman(O'Reilly)著作。版权 2019 年 Greg Foss 和 Paul Modderman,978-1-492-04644-8。”

如果您认为您使用的代码示例超出了合理使用范围或上述许可,请随时联系我们:permissions@oreilly.com

O’Reilly 在线学*

注意

* 40 年来,O’Reilly Media 提供技术和商业培训、知识和洞察,帮助企业取得成功。

我们独特的专家和创新者网络通过书籍、文章、会议和我们的在线学*平台分享他们的知识和专业知识。O’Reilly 的在线学*平台为您提供按需访问的实时培训课程、深度学*路径、交互式编码环境以及来自 O’Reilly 和其他 200 多家出版商的大量文本和视频。欲了解更多信息,请访问:http://oreilly.com

如何联系我们

请将有关本书的评论和问题发送给出版商:

  • O’Reilly Media, Inc.

  • 1005 Gravenstein Highway North

  • Sebastopol, CA 95472

  • 800-998-9938(美国或加拿大)

  • 707-829-0515(国际或本地)

  • 707-829-0104(传真)

我们为本书设立了一个网页,列出了勘误、示例和任何额外信息。您可以访问此页面:https://oreil.ly/practical-data-sci-sap

要就本书发表评论或提出技术问题,请发送电子邮件至:bookquestions@oreilly.com

有关我们的图书、课程、会议和新闻的更多信息,请访问我们的网站:http://www.oreilly.com

在 Facebook 上找到我们:http://facebook.com/oreilly

在 Twitter 上关注我们:http://twitter.com/oreillymedia

在 YouTube 上观看我们:http://www.youtube.com/oreillymedia

致谢

我们要感谢我们的技术审阅人员 Hau Ngo、Jesse Stiff、Franco Rizzo、Brad Barker 和 Christoph Wertz 提供的宝贵反馈。他们的建议使每一章都变得更加出色。

致我们无畏的编辑 Nicole:在整个过程中,您帮助我们保持冷静和脚踏实地。没有您的编辑指导,我们在数据科学的曲折和代码的咆哮中会迷失方向。感谢您使您触及的每件事情都更加易读。

Greg 想要感谢他的妻子 Alycia,感谢她的耐心、支持和洞察力,以及他的兄弟 Cory 在图形方面的帮助。当然,巨大的感谢还要送给 Paul Modderman,因为他的远见、创造力和踏上这段旅程的勇气。

保罗要感谢他的伴侣克里斯塔·莫德曼,感谢她的智慧和力量,感谢他的祖母罗伊丝·斯特拉特曼,在创作工作中展现出的非凡生活榜样,以及他的父母马克和琳达,因为...一切。他还想感谢托尼·范德波尔、迪恩·斯托夫尔和加文·奎因分别对他的职业进步给予鼓励、信任和启发。最大的感谢要送给格雷格·福斯:一位从未在追求质量的承诺中退缩的杰出作者。埃莉诺·莫德曼是他永远的最爱。

特别感谢韦德·克兹马尔齐克在 CRM 场景方面的帮助。

¹ 如果您不是那种享受数据乐趣的人,您是如何找到这本书的呢?

第一章:引言

用数据讲好故事

我们关于放弃垃圾的说法还远远不够。

伊拉·格拉斯

我们都见过它们。那些令人生畏的 PowerPoint 演示文稿,屏幕上滚动着一队队的项目符号。讲师经常会为繁忙的幻灯片道歉,然后继续按照幻灯片上印刷的每个字读。您开始怀疑自己是否昨晚忘了关炉子。我们都喜欢故事。一个结构良好的叙事,无论是电影、书籍、电视节目还是播客,都会像毯子一样缠绕在我们周围,吸引我们的注意力。那些布满项目符号的 PowerPoint 幻灯片……却不是这样。随着互联网和物联网的兴起带来的数据洪流,我们很容易在演示中加入一些发现,擦干净我们的手,然后说“就是这样了”。然而,作为数据专业人士,我们不能只是向受众“淋浴”数据发现。流行的建议是,您必须用数据讲述一个故事——确保它是人们愿意听的引人入胜的故事。不要否认讲述故事能带来的乐趣。

要讲述一个引人入胜的故事,您必须首先识别它。我的数据被要求展示什么?我的用户正在寻找什么洞察?专门提供服务和设备的公司可能会问:“哪些设备最需要维修?哪些最不需要?设备类型与零件更换之间是否存在相关性?”在同一家公司,财务部门的某人可能会问:“我们如何更准确地预测手头的现金?”在销售部门,问题可能是:“我有什么样的客户流失情况?”

确定了您的故事之后,您需要找到您的受众。有多种方法可以细分受众,但通常您的受众包括高管、业务专业人员和技术专业人员。尽管他们可能管理或指导许多业务流程,但高管通常对这些流程的日常运行了解甚少。细节对他们来说无关紧要(或可能令人困惑)——他们想要看到大大的粗体字。业务专业人员是业务流程的日常管理员,如超级用户和业务分析师。他们了解流程的详细信息,并能理解原始的表格数据。技术专业人员是您受众中最小的部分;他们通常包括数据分析和数据科学团队中的同事。这个群体需要较少的业务和流程背景,而更多的是如回归的均方根误差或神经网络的架构等技术细节。

一旦你确定了故事和受众,你就需要继续进行旅程中最困难和不确定的部分:寻找数据。如果没有数据来支持你的故事,你的旅程将很快结束。假设你想讲述太阳黑子如何与北半球的帽子和手套销售相关的故事。令人惊讶的是,太阳黑子数据很容易获取。你已经有了这部分数据。然而,你只有关于帽子销售的细节,没有手套的数据。这里需要谨慎。你是改变你的故事以适应数据,还是放弃并找到另一个故事?反向过程是可行的,但这是一条很险的路。一般的规则是,不要改变你的假设来适应你的数据。

在你完全信任这些数据之前,你需要对其进行审核,并开始问很多问题:

数据源是否可靠?你是从网站上的一个表中爬取数据吗?该网站使用了什么数据源,并且数据是如何获取的?像 Data.gov、ProPublica、美国人口普查局和 GapMinder 这样的来源是可信的,但其他来源可能需要小心处理。

你有太多数据吗?是否有易于识别的无用特征?寻找明显相关的特征。在前面提到的太阳黑子数据中,也许你有一个 UTC 时间戳特征和另外两个日期和时间的特征。要么日期和时间应该被抛弃,要么时间戳。我们稍后将讨论的技术可以帮助你快速查看相关性,以帮助你确定何时两个特征过于相关,两者都不再有用。

数据是否完整?使用一些初步的数据工具确保你的数据没有太多遗漏的信息。我们稍后会详细讨论这个过程。

故事已经定好,受众已确认,数据已审核,接下来怎么办?现在你准备好进行故事的艺术和乐趣了——确定使用什么工具来支持或拒绝你的零假设。说你在使用“数据科学”作为工具是一条很险的路。你有先进的报告、机器学*和深度学*在你的工具库中。通常,只需将数据组织成易于使用的仪表板,就能讲述整个故事。不需要再做更多。尽管这在我们的职业生涯中曾让我们沮丧过,但这种情况发生的次数比其他任何情况都多。我们开始旅程时认为我们有一个适用于递归神经网络的案例,其中有门控循环单元或长短期记忆模块。在收集数据时,兴奋感不断增加。然后我们意识到支持向量机或简单的回归也能胜任。后来,有些失望地意识到,为用户提供探索数据的仪表板已经足够讲述故事。并非所有情况都需要深度甚至机器学*。尽管这通常很有趣,但将你的故事硬套到这些范式中通常并不能更好地讲述故事。

最后,花点时间学*一下讲故事的艺术。即使是我们干燥的数据科学故事也值得一些关注和关注。艾拉·格拉斯是一个了不起的讲故事者。他在讲述故事艺术方面有一系列四段短视频。观看它们,并将他的智慧建议融入到你的故事中。

一个快速的观察:SAP 专业人士的数据科学

SAP 专业人士每天都在忙于支持业务和用户,不断寻找流程改进。他们收集需求,在 SAP 系统中进行配置或编码,并且往往生活在 SAP GUI 中。他们对 SAP 内的数据以及业务流程有着深入的了解,并可以像念咒语一样召唤一大堆事务代码。当要求提供带有分析的报告时,他们确实有两个选择:在 SAP 中编写报告或将数据推送到数据仓库,由其他人生成报告。这两个过程通常是长期的、资源密集型的努力,给最终用户和 SAP 专业人员带来沮丧。

阅读本书将帮助你——作为 SAP 专业人士——在业务专业人士和数据科学家的世界之间架起一座桥梁。在这些页面中,你会找到一些打破以往受限制的典型报告和/或分析方法的想法。正如我们之前讨论的那样,做到这一点的首要方法之一就是提出更好的问题

这是一个典型的 SAP 场景:辛迪在应收账款部门工作。她需要一个 30-60-90 天逾期报告,列出逾期客户并根据逾期时间将其分组到 30 天、60 天或 90 天逾期的桶中。财务部门的莎伦收到请求,她知道可以创建一个标准的 ALV(ABAP 列表查看器)报告,或者提取数据并将其推送到业务仓库(BW),在那里使用 Microstrategy 或其他工具生成报告。

如果我们将莎伦的视角转移到数据科学家的角度会怎样呢?莎伦收到报告请求。她知道她可以提供刚刚被请求的内容,但是她想:“还能做些什么?”她打开记事本,记下了一些想法。

是否存在拖欠付款的惯犯?

数据中是否存在任何有趣的相关性?我们知道客户名称、客户付款历史、客户购买和金额。

能预测一个人会延迟支付吗?延迟多久?

我们可以使用这些数据来帮助评估我们的客户吗?评级较低的客户在库存不足时可能无法下订单,而评级较高的客户则可以提出同样的请求。

哪些可视化类型会有帮助?

Sharon 概述了一个交互式仪表盘报告,她认为对用户非常有用。凭借这些想法和草图,Sharon 向部门数据科学家(或 SAP 开发人员)询问可能性。

在这里存在明显的方法差异。第一种是典型的 SAP 响应,限制了业务分析师的创造力和智力能力。第二种则利用了他们的创造力。当 Sharon 在 SAP 中看到数据并提出更好的问题时,她将在实质性流程改进中发挥重要作用。

这只是一个例子。想象一下典型 SAP 团队接收到的所有请求及其可能性,因此这本书出现了!

另一种改变 SAP 团队思维,使其更加动态和数据中心化的方法是使用更好的工具。这是 SAP 开发人员的责任。大多数 SAP 开发人员生活在名为 ABAP(高级业务应用程序编程)的应用程序编程语言的世界中,当被要求提供报告或流程改进时,立即转向 SAP GUI 或 Eclipse。这是他们被期望花费时间并提供价值的地方。

小贴士

ABAP 最初被称为Allgemeiner Berichts-Aufbereitungs-Prozessor。它是一种专门设计用于扩展 SAP 核心功能的服务器端语言。您可以创建显示报告、运行业务交易或接收外部系统数据并集成到 SAP 中的程序。大量 SAP ERP 交易完全依赖 ABAP 代码运行。

ABAP 开发人员经常专注于 SAP 提供的一个或多个业务功能之一。由于 ABAP 程序经常直接增强标准 SAP 功能,ABAP 开发人员对企业如何设计其流程非常熟悉。熟悉 ABAP 的人很常见同时承担技术编程角色和业务分析角色。

小贴士

SAP 开发人员,我们恳请您:将 SAP 视为数据源。报告的呈现层和逻辑层应该从数据库层抽象出来(见图 1-1)。值得注意的是,SAP 数据具有高度结构化和严格的业务规则。采用这种方法的一个最明显的优势是逻辑层可以访问其他数据源,例如公共数据。在 SAP 系统内部,如果需要查看雨靴销售与天气模式之间的关联,NOAA 的天气数据必须被带入 BI 或 SAP 本身。然而,通过使用分层模型,数据可以被逻辑层访问并在呈现层中呈现。通常情况下,数据可能是 API,允许在没有存储的情况下访问。这种模型还允许逻辑层与像 Azure 机器学*工作室这样的工具进行绑定,对 SAP 数据执行机器学*或深度学*。

图 1-1. 数据科学发现的简单分层方法:数据库、逻辑和数据呈现

SAP 缺乏 Python 中数以千计的库或 R 中数以千计的包。¹ 它也缺乏轻松创建动态/交互式仪表盘和可视化的能力。不要误会:SAP 确实有工具进行高级分析、仪表盘和可视化。只是这些工具成本高,需要大量时间和精力。有些地方需要几个月甚至几个季度才能创建报告,有时甚至有效的商业问题时间只有几小时。通过本书中的工具,我们打算弥补这一差距。如果你是 SAP 开发人员,我们强烈建议你学* Python 和 R 等编程语言,以便用它们来对 SAP 数据进行分析。首先,它们不局限于 SAP 生态系统,其次,它们是免费的。

除了 SAP 之外,还有许多其他工具可帮助 SAP 开发人员展示其 SAP 数据。你可以在 R 中使用 RMarkdown,在 Python 中使用 Shiny 和 Jupyter Notebooks,还有 PowerBI、Tableau、Plotly 等等。在本书中,我们将使用 PowerBI、RMarkdown 和 Jupyter Notebooks 作为演示示例。

简介:数据科学家的 SAP 基础知识

尽管 SAP 如此之大且无处不在,其缺乏广泛的认知常常令人惊讶。一个惊人的事实是:全球 77%的交易收入以某种方式与 SAP 系统相关。如果你花钱,很可能你已经与 SAP 打过交道。而 92%的《福布斯》全球 2000 强企业是 SAP 的客户。

但是 SAP 软件究竟如何触及所有这些?它到底做了什么?*年来,SAP 通过收购多家 SaaS(软件即服务)公司来扩展其产品组合并使股东获益,但它最初的核心专注点是 ERP:企业资源规划。

SAP 成立于 1972 年,最初被称为 Systemanalyse und Programmentwicklung,源自德国。在 IBM 服务器上运行 DOS 系统,首个功能是一个后台财务会计软件包。很快,相继推出了采购、库存管理和发票验证模块。可以看出其主题:完成企业日常需求。

起初,这些功能列表可能看起来相当乏味,尤其对于我们这些拥有 Python 模块和 TensorWhatsits 的酷炫数据科学家来说,我们知道如何让计算机告诉我们图片中有狗(但没有飞机)。它不像搜索 Google 或者在 iPhone 上使用 Siri 那样神奇。但 SAP 在这些乏味的初始模块中添加了一个变数:集成。库存管理直接影响采购,采购直接影响财务,财务直接影响……嗯,一切。这一个 SAP ERP 系统包含了所有这些模块。现在,公司不再需要购买和运行单独的财务/库存/发票系统,从而节省了大量资金。当一个系统可以提供所有业务问题的答案时,客户开始大量购买。这就是 ERP 的价值和胜利。到了 1990 年代,当 Gartner 创造了 ERP 这个术语时,SAP 的年销售额已经超过 10 亿德国马克。

注意

由于全球大多数大公司在如此多的业务关键功能中使用 SAP,难怪如此多的业务可以在其中进行。

从 SAP 获取数据

像大多数大型商业应用程序一样,SAP ERP 使用关系数据库来存储事务数据和主数据。它设计得让客户可以从多种关系数据库管理系统(RDBMS)中选择作为 SAP 应用程序数据库。Microsoft SQL Server、IBM DB2、Oracle 和 SAP 的 MaxDB 都得到支持。在过去几年中,SAP 迅速引入了另一种专有的数据库技术——HANA,作为一种具有内存技术的关系数据库管理系统解决方案。虽然未来版本的 SAP 核心 ERP 产品有朝一日将要求使用 HANA,但今天大多数的 SAP 安装仍然使用其他技术作为其数据库。

提示

在本书中,我们将介绍几种从你的 SAP 系统中提取数据的方法,这些方法都不需要你准确地知道你的 SAP 系统运行在哪种数据库上。但是如果你是一个真正的极客,你最终还是会弄清楚的。

在你公司的 SAP 实例中驱动关系数据库都是巨大且充满了事务和主数据。它们完全描述了由 SAP 存储和处理的关键业务信息的形状。你的 SAP 系统核心的数据库是你可以进行发现的真实来源。

除非它是你的绝对最后选择,否则你永远不应该直接连接它们。

好吧,我们在这里有点在开玩笑。你会找到合适的时机通过 SQL 语句直接从 SAP 数据库查询数据。但是数据模型的巨大规模和难以置信的复杂性使得完全理解一个简单的销售订单的结构可能涉及超过 40 个表和 1000 多个字段。即使是 SAP 的黑带也很难记住所有他们需要使用的各种表和字段,所以想象一下对于一个刚接触 SAP 的数据科学家来说,解开所有这些必要数据的各种要素会是多么低效。

BAPIs:使用 NetWeaver RFC 库

数据爱好者们,如果对 SAP 不太熟悉,应该从检查 SAP 系统中可用的业务应用程序接口(BAPIs)开始。BAPIs 是由 SAP 提供的远程调用函数,用于公开各种业务信息文档中的数据。不必去弄清楚 40 多个销售订单表中哪些适用于你的特定数据问题,可以查看各种销售订单 BAPIs 的结构,确定它们是否填补了这个空缺。逆向工程数据模型的麻烦不复存在。

BAPIs 还通过覆盖早期版本中的系统限制来提供帮助。在 SAP 核心产品开发的早期阶段,各种模块限制了可以表示表或字段的字符数。多年来,随着 SAP 的显著稳定性,这些表和字段名称一直保留下来。如果不在 SAP 内部工作,你怎么可能知道“LIKP”和“VBELN”与交货数据有关?BAPIs 是后来的补充,因此它们已经通过更好地描述其形状和功能的接口成长起来。

OData

SAP NetWeaver Gateway 代表 SAP 进入现代 Web 时代的许多方式之一。它是一个 SAP 模块,在某些情况下运行足够自己的东西以至于值得作为一个独立系统,允许 SAP 开发人员快速轻松地建立 HTTP 连接到 SAP 后端业务数据。我们预测你将在第六章看到使用 SAP NetWeaver Gateway 的例子。

运输的基础层称为 OData。OData 代表许多技术公司联合起来通过 RESTful API 在 Web 上进行通信的一种标准方式。它提供了一个通用的数据格式,使用 XML 或 JSON 通过 Web 传输数据的方式,为客户端指示基本的创建/读取/更新/删除操作,并通过基于 XML 的方法让服务器指定与数据交互时提供的字段、结构和选项的元数据。

通过 SAP NetWeaver Gateway 使用 OData 需要使用 SAP 的原生后端语言 ABAP 进行编程。我们一些熟悉这种语言的 SAP 原生读者可以生成 Gateway OData API。其他读者可能不熟悉,但可以放心:如果你的公司在任何有意义的方式上运行 SAP,公司内会有了解 ABAP 的人员。这些人要么知道如何创建 OData 服务,要么能够快速学*,因为这并不难。

当您找不到满足数据需求的 BAPI 时,请选择 OData。这是一个很好的中间地带,为 SAP 管理员提供了灵活性来计量和监控其使用情况。它还赋予开发人员以任意方式组合数据的能力。使用 OData 的另一个好处是,它不像 BAPI 方法那样需要 NetWeaver 连接器:任何能够在企业网络内安全进行 HTTP 请求的设备都能够进行 OData 请求。

获取数据的其他方法

如果您找不到合适的 BAPI,并且找不到资源来制作 OData 服务,总是还有几条其他路线可供选择。由于这些通常不是我们推荐的事项,我们将简要涵盖它们。

Web 服务

SAP 允许您基于其 Internet Communication Manager (ICM)层创建 Web 服务。这些 Web 服务使您比 OData 更灵活地工作,但仍需要 ABAP 知识。介于 Gateway 和完全自定义的 SAP Web 服务之间的空间很小,请仔细考虑您的数据问题是否无法通过 OData 来回答。

直接数据库访问

每个人都说你不应该这样做,但我们也都遇到过一两次这是唯一行得通的情况。如果您必须选择这条路线,一个关键任务将是确保您提取的数据与 SAP 向最终用户提供的数据匹配。许多时候,隐藏的输入/输出转换和数据建模层不会在仅通过数据模型浏览时显现出来。

严肃地说,直接从 SAP 数据库中选择数据就像是驾驶一辆有刹车问题的方程式赛车。你会快速到达目的地,但可能在路上撞上一两堵墙。

将屏幕截图导出到 Excel

有时候,最终用户会准确地知道哪个屏幕上有他们需要的数据。许多时候,这个屏幕会有一种机制,可以将数据导出到 Excel 中。

角色和责任

数据科学结合了各种技能集。这些技能通常包括统计学、编程、机器学*、分析、架构和工程。许多博客和在线帖子讨论数据科学角色之间的差异。有无数的职位标题和界定。一个阵营将角色定义为数据分析师、数据工程师、数据架构师、数据科学家和数据通用角色。其他组织有他们自己的划分。

读者应该明白一件非常重要的事情。除非你在一个非常大的公司并且拥有一个数据科学团队,否则你很幸运能有一个人拥有这些技能。这些职位划分在理论上存在于所有人,但实际上只有少部分人。准备好做很多事情。如果你在公司应用了一些数据科学的尝试,准备好自己动手。没有 SQL 数据库想要提取和存储一些 SAP 数据?我们来介绍一下。想要自动化工作流程以进行数据提取?这里给你。从 SAP 数据到呈现层,我们将涵盖所有内容。

我们的意图很明确:我们希望培养懂得如何在他们的组织中进行数据科学工作的公民数据科学家。你可能没有任何资源来帮助你,当你请求这些资源时可能会遇到阻力。通常情况下,你必须在得到帮助之前证明你的理论。我们理解角色和责任并没有很好地定义。我们希望给你一个概览。如果你正在阅读本书,那么你已经卷起袖子准备做从构建 SQL 数据库到在 PowerBI 中呈现机器学*结果的所有事情。

总结

获取价值的一个重要部分是进行沟通。我们讨论了如何使用在 SAP 中找到的数据讲述精彩故事:确认你的故事,找到受众,发现数据,并对这些发现的数据应用严格的工具。有时,仅仅需要一个简单的图表来传达故事。其他时候可能需要详细的结果列表。但无论用什么视觉方法来传达你的发现,都要准备好用它来讲故事。

想要讲述关于他们数据的故事的 SAP 专业人员应该看看像 Python 和 R 这样的编程语言,以及 Tableau 和 Power BI 这样的可视化工具。深入了解请参考第二章。

想要发现 SAP 中有什么的数据科学家应该看看获取这些数据的方法。BAPI 提供了一种基于功能的数据检索方法,OData 设置了可重复和可预测的 HTTP 服务,作为最后的手段,你总是可以将屏幕数据导出到 Excel 或直接查询 SAP 数据库。详细内容请参考第三章。

我们希望你能充分利用企业中可供挖掘的 SAP 数据,并且从原始数据中获取价值的最佳方式是应用数据科学原理。本书将向你展示如何将 SAP 的世界与数据科学的世界结合起来。

¹ 想要体验 R 软件包领域有多广阔,可以看看这篇博客文章,了解软件包列表的增长和寻找合适软件包的搜索策略。

² 但是,如果我们不承认最糟糕的黑客和不明智的胶带解决方案至少占据了任何真实环境的 50%,那么这本书就不能被称为“实用”。

第二章:数据科学与 SAP 专业人员

注意

如果你是一名数据科学家,你可能不需要本章节中的大部分信息。我们试图让 SAP 专业人员迅速掌握你可能已经了解的内容。

作为一名 SAP 业务分析师,Fred 始终在寻找流程改进的方法。那是他的工作,他做得很好。他听说过很多关于数据科学的信息,但对他来说,那只是...信息噪音。数据科学是创造自动驾驶汽车、击败围棋世界冠军和翻译语言。Fred 在一家美国制造商工作,数据科学对他来说并没有真正的相关性。

或者呢?

如果 Fred 了解数据科学的基本概念,他就会明白如何利用它为业务提供价值。最*,他与产品开发团队合作,后者正在寻求 IT 的帮助来简化他们的流程。他们有大量未组织的数据。他们向 Fred 提出了一个想法,一个仪表板来帮助他们追踪他们的流程。当 Fred 评估项目时,他的第一个反应是将数据放入 SQL 数据库中。一旦放在那里,他可以使用像 PowerBI 这样的演示工具创建一个仪表板。这是一个每个人都喜欢的解决方案。

Fred 并不了解数据科学的基础知识。这些数据中的特征可能帮助公司做出更好的、基于数据的决策。如果他了解回归和聚类的基本概念,他就会明白。他会知道他可以做更多,而不仅仅是项目团队要求的那些业务数据。

这正是本章的重点所在。我们不是要培养数据科学家。我们试图让业务分析师像数据科学家一样思考一点;我们试图创建公民数据科学家。这些是理解数据科学足够多的业务分析师和专业人士,他们能够提出关于如何将数据科学应用于他们的数据(特别是对他们的SAP数据有用的)的问题。为此,我们需要介绍数据科学的基础知识,包括不同类型的学*模型:机器学*和神经网络。

接下来将会对该主题进行一场快速浏览,至少会给你留下足够的信息,让你以稍微不同的方式思考业务流程...以数据科学的方式。理想情况下,你可以考虑你的项目和数据,并对你的数据科学家或开发者说:“也许朴素贝叶斯这样的分类算法在这个问题上可能有效。”想象一下这种回应将引起的震惊!

这是一个概念性的章节,提供了主要数据科学概念的概述,因此我们不会讨论诸如探索性数据分析(EDA)或数据准备等具体的想法。我们已经涵盖了我们认为最相关的主题,但可以轻松地争论我们忽略了重要的事物,例如自动化机器学*(autoML)和集成方法;然而,我们必须在某个地方划定界限,以便保持本章的可管理性。尽管如此,我们稍后将讨论诸如 EDA(在第四章中讨论)等具体概念,所以请继续关注。

机器学*

数据科学中的语法可能令人困惑且重叠。根据定义,深度学*是机器学*的一个组成部分,但我们指的深度学*是使用更复杂的神经网络模型。深度学*需要更多的计算能力、时间和数据才能取得成功。通常情况下,更简单的机器学*模型表现同样甚至更好。在光鲜和花哨的神经网络面前,不要忽视它们。

注意

大多数数据科学家的时间主要用于查找、清理和组织大量数据。一些估计称,数据科学家花费 80%的时间在这个无趣的任务上。对于查看 SAP 数据的数据科学家,我们有好消息。SAP 是一个 ERP 系统。数百万行的业务数据已经在关系数据库中。虽然这并没有结束进行一些清理和重新组织的必要性,但确实减少了这些工作的努力。我们将展示如何查找和提取这些数据,但通常情况下,几乎不需要进行清理或组织。

机器学*大致可以分为四类:

  • 监督学*

  • 无监督学*

  • 半监督学*

  • 强化学*

小贴士

深度学*也包括这些类别。它被认为是机器学*的一个子集。对于本书的目的,我们在这里提到机器学*而不是深度学*的子集。我们稍后会介绍深度学*。术语中存在大量的重叠和混淆。如果你关注机器学*的新闻,你会发现地球上没有两个人使用相同的术语方式——所以不要因为感到困惑而感到难过。

监督学*机器学*

监督学*是在有标记的数据上进行的。它在分类方面表现出色,这是一种为一组数据分类或预测分类标签的方法。例如,在营销中,可以确定购买产品的客户。监督学*还在预测方面表现良好。预测是从一组数据中确定数值的方法。以与分类相同的类比,例如在营销中,可以用来尝试确定客户可能花费多少。例如,著名的鸢尾花数据集包含有关 150 朵鸢尾花的花瓣长度、花瓣宽度、萼片长度和萼片宽度的信息,并确定它们的物种。一旦我们对这些数据进行模型训练,它就能准确预测一朵新鸢尾花的物种,只要给出其萼片和花瓣数据。让我们更详细地了解一些不同类型的监督学*模型。

线性回归

线性回归是一种建模因变量与一个或多个解释变量之间关系的方法。一个房屋价值与其面积的关系是一个很好的例子(图 2-1)。如果你有几个房屋的价值和它们各自的平方英尺,你可以推测出一个未知房屋的价值,只要知道它的大小。当然,一个房屋的价值不仅仅取决于这些因素,但你明白我的意思。

图 2-1. 房屋价格与平方英尺的线性回归

逻辑回归

逻辑回归与线性回归类似,使用相同的基本公式。然而,逻辑回归是分类的,而线性回归是连续的。以同样的房屋价值例子,线性回归用于确定房屋的价值,而逻辑回归可以用于确定是否会卖出。

决策树

决策树是一种简单地提出问题并做出决策的模型。决策树的节点提出问题,导致要么到其他节点,要么到末端节点(叶子),这些末端节点是分类或预测(图 2-2)。

图 2-2. 吃饼干的决策树

随机森林

随机森林是一组决策树,帮助解决决策树的一个最大问题:过度拟合(图 2-3)。过度拟合模型意味着它非常擅长解决它已知的问题,但当引入新数据时,它会表现不佳。可以将其视为训练自己成为世界一流的一级方程式赛车手,但从未学会停车。¹

图 2-3. 随机森林

无监督机器学*

正如你可能猜到的那样,无监督机器学*没有标记的数据。也就是说,你有一堆数据,但你不知道输出标签。例如,你有一组包含年龄、性别、收入、职业和其他特征的选民记录。你不知道的是它们之间的关系。让我们看一下一些不同类型的无监督机器学*。

k-均值聚类

k-均值聚类将数据分组到给定的点集中(图 2-4)。一个例子是将一组客户分组或聚类到代表他们购买频率的组中。它通过将它们与最*的均值值进行分组来完成此操作。如果使用非欧几里德距离,如 Levenshtein,也适用于单词。我们将在第七章中详细介绍这一点(ch07.html#clustering_and_segmentation_in_r)。

图 2-4. 聚类

朴素贝叶斯

朴素贝叶斯不是单一的算法,而是贝叶斯定理家族中的一系列分类算法(图 2-5)。其共同概念是数据的每个特征都被视为独立于其他特征。例如,一辆汽车有车盖、后备箱、轮子和座位。朴素贝叶斯将这些都视为独立的贡献因素,判断这个对象是一辆汽车的概率。朴素贝叶斯非常快速,通常是机器学*任务中首选的分类器。

图 2-5. 贝叶斯定理

这里是贝叶斯定理的术语,用简单的语言来说:

P(c | x)

假设成立(c)的概率,给定数据(x)。

P(x | c)

如果假设成立(c),数据的概率(x)。

P(c)

假设成立的概率(c),无论数据如何。

P(x)

数据的概率(x),无论数据如何。

这是贝叶斯的一个常见解释;它无处不在。然而,这有点难以理解,所以让我们简化一下。

有一个非常常见和直观的解释贝叶斯的例子,使用乳腺癌作为例子。考虑这种情况:患者去医生那里检查,乳房 X 线照片结果异常。那么患者患有癌症的可能性有多大?你可能直觉地认为癌症一定存在,因为测试结果,但将贝叶斯应用到这种情况显示了不同的结果。让我们来看一下。

考虑这些统计数据:³

  • 参加例行筛查的 40 岁女性中,有 1%患乳腺癌,99%没有。

  • 当乳房癌存在时,80%的乳房 X 线照片将检测到癌症,而 20%未能检测到。

  • 9.5%的乳房 X 线照片返回假阳性;它们检测到了实际上不存在的癌症。这意味着 89.5%未能检测到癌症,实际上也没有癌症(真阴性)。

  • 事件的概率是事件发生的次数除以所有可能性。

    P(c|x) = .01 * .8 / (.99 * .095) + (.01 * .8) = .0776

直觉上听到乳腺 X 光检查的准确率为 80%,因此如果结果呈阳性,则表示你有 80%的患癌症的几率。但事实是……即使结果呈阳性,你只有 7.8%的概率患癌。

分层聚类

分层聚类是一种将结果分组成树状图或树状图的方法(图 2-6)。如果从许多集群开始并移动到一个集群,则称为分裂。如果从一个集群开始并移动到多个集群,则称为聚合。分裂方法通过计算其特征之间的最大差异(或距离)来分区给定的集群。聚合方法则相反。它计算所有集群之间的差异,并组合具有最少公共特征距离的两个集群。它们都会继续进行,直到数据用尽或树状图达到预定义的分裂次数。我们将在第 7 章中更详细地讨论。

图 2-6. 聚合和分割层次聚类

半监督机器学*

半监督机器学*是监督学*和无监督学*的结合。在这种情况下,您有大量数据,但并非所有数据都已标记。考虑欺诈检测的情况。信用卡公司和银行拥有大量的交易数据,其中一些已正确标记为欺诈。然而,并不是所有的欺诈交易都被发现。理想情况下,他们会手动正确标记所有的欺诈交易。然而,这个过程是不切实际的,需要太多时间和精力。已有一小部分标记数据和非常大量的未标记数据。在半监督学*中,一个常见的技术称为伪标记。在这个过程中,使用传统的监督学*方法对标记数据进行建模。一旦模型建立并调整好,将未标记数据输入模型并进行标记。最后,使用已标记数据和新伪标记的数据再次训练模型(图 2-7)。

图 2-7. 用于半监督学*的伪标记

强化机器学*

强化机器学*是指训练模型根据试错来做决策。该模型通过从过去的成功和失败中学*来与环境进行交互。然后,它确定下一次尝试或迭代的行动方案。它的工作原理是最大化奖励。最常见的例子是训练机器玩游戏。让我们更详细地看看几种不同类型的强化学*。

隐马尔可夫模型

隐藏马尔可夫模型(HMMs)是一系列可观察的发射。这些是模型通过的给定状态的结果,用来进行这些发射。这有点令人困惑,所以让我们澄清一下。在 HMM 中,你不能直接观察到状态,但你可以观察到那些状态的结果。你在一个没有窗户的办公室工作,看不到外面的天气。你可以看到人们来办公室时穿着什么。比如说 75%的人带着雨伞...你可以推测外面正在下雨。HMMs 是识别序列和时间序列的流行方法。它们不看真实的状态;相反,它们看真实状态的发射。最简单的模型假设每个观察是独立的。然而,HMMs 假设观察之间存在关系。再举个例子,一系列数据被观察到了天气。这些数据中包含了气压、温度和一年中的日期等特征。相应的发射数据具有“不多云”或“多云”的二元特征。观察多天的连续性,该模型不仅预测今天的可观察特征的天气状态,还预测了前几天的特征的天气状态。HMMs 试图识别最有可能的潜在未知序列,以解释观察到的序列。

这个概念有点棘手,所以让我们用另一个例子来说明。比如你想用 HMM 来确定公司的订单是否增加或减少购买 widgets。SAP 有订单数据的历史记录和时间戳。它还有其他状态可能会影响 widgets 购买的时间。有销售订单、年度时间(季节性)、仓库库存水平和生产订单。每一个这些都可以被 HMM 使用。可以这样理解:“过去的行为预测未来的行为。”

Q-learning

Q-learning 是一种基于价值的强化学*算法。它基于一个行动的质量。Q-learning 通过学*优化其结果的步骤进行。在游戏玩法的例子中,它执行一个动作,评估该动作,更新其行为秘笈(cheat sheet)表明该动作的好坏,然后再次尝试。它以极快的速度迭代这个过程。

Figure 2-8. Q-learning 步骤

一种常见的插图是想象一个游戏,你是一只狗,你必须找到骨头堆。每走一步花费一根骨头。如果你遇到那只讨厌的猫,你会失去 10 根骨头并且死亡(Figure 2-9)。目标是最大化骨头数量。

Figure 2-9. Q-learning 狗最大化优化骨头

对我们来说,这似乎是一个简单的游戏,但计算机不知道该如何开始。所以它首先下去拿了两根骨头。耶!哥们,这一步走得好。它记录下来并向右走了一步。该死的猫……游戏结束。它更新了作弊表格的信息。下次它先向右走一步,然后再向右走一步,然后只有向下的选项。是的——骨头的“金矿”!记住每走一步要付出-1 个骨头的代价。结果是-1 +2 -1 -1 +1 -1 -1 +10 = 8。它记录下结果并再试一次。这次它向右走,因为知道那里有+1。它再向右走一步,然后向下找到“金矿”。结果是-1 +1 -1 -1 +10 = 8。两条路径同样有价值,但如果有奖励或步数限制,第二个选项胜出。

你可能会想,“挺酷的,但这与游戏之外的应用有何关系?” 想象一下斗牛犬找到主要金矿的情景。现在把它想象成一个简单的仓库……大幅扩展它(图 2-10)。强化学*可以减少拣选、打包和库存的运输时间,同时优化空间利用率。虽然更复杂,但基本上与狗和骨头的游戏相同。

图 2-10. 这个仓库比狗找骨头更复杂,但通过强化学*的路径规划同样适用于这里

神经网络

我们两个作者都从事编程多年,并在这过程中体验过一些美妙的“哇”时刻。格雷格通过 Apple IIe 上的 Basic 学会了编程。在学会使用 PEEK、POKE 和 CALL 命令之前,他已经编程了大约一年。第一次使用这些命令并运行他的程序时,他坐下来想:“哇!”;从那以后,他一直以某种形式进行编程。当格雷格和保罗编写他们的前几个深度学*程序时,他们都有这种感觉。“哇!”是我们能说的全部。

让我们谈谈深度学*及其含义。

传统编程遵循简单直接的逻辑故事。如果这样,那么执行那个动作 10 次。它如此强大,我们可以模拟美丽的风景并创建让我们陶醉的游戏,带我们进入魔法般的、想象的境界。但这使得像语言翻译这样的任务几乎不可能。想象一下需要翻译英语到韩语的程序。这个程序需要考虑词语、短语、否定、语法、本土用语、标点符号等等,无穷尽的条件。想象将所有这些嵌套在线性逻辑中。机器学*出现了。现在你输入一组英文文本及其翻译成韩文的等价文本。你通过展示输入和期望输出来训练模型。你拥有的数据越多,你就能训练模型得越好。最后,你输入一组没有韩文翻译的英文文本,哇!它像学会了一样进行翻译。

这本身就很了不起,但更好的是,谷歌在 2016 年构建了一个深度学*算法,能够从英语翻译成韩语、韩语翻译成英语、日语翻译成英语以及英语翻译成日语。这本身就非常了不起——但这不是令人惊奇的部分。该网络能够直接从日语翻译成韩语和韩语翻译成日语,而无需先经过英语。想一想这是如何发生的?网络内部发生了什么,使得这种事情成为可能?该网络学会了一种元语言——一种超越简单一对一语言翻译的语言映射。

当从日语翻译成韩语时,人们期望模型首先经过英语(弯曲的线路);见 图 2-11。然而,模型并没有这样做。它直接从日语到韩语(虚线路)。令人惊讶!实际上有点令人毛骨悚然。

图 2-11. 谷歌的语言翻译器

谷歌的语言翻译器是一个正在运行的神经网络。让我们来看几种基本的神经网络架构。这是神经网络和深度学*的初步介绍。我们希望这能激发你的好奇心,让你有兴趣深入了解。神经网络的基础是一系列相互连接的处理模块,它们一起接收输入并求解给定的输出。它们受到大脑中神经元和突触处理信息的启发。它们在解决从图像分类⁴到语言翻译等问题上发挥了重要作用。我们将在第九章中进一步深入探讨这一点 Chapter 9。

神经网络有三个基本层:

输入层

这里是数据输入到网络的地方。

隐藏层

这一层执行基本计算,然后将权重传递到下一层。下一层可以是另一个隐藏层或输出层。

输出层

这是网络的结束,模型输出结果的地方。

神经网络有六个基本概念,如下节所述。

前向传播

数据(权重和偏差)从输入层经过各个隐藏层向前流动,最终到达输出层(图 2-12)。

图 2-12. 前向传播

反向传播

数据经过网络前向传播后,误差(期望值减去获得值)通过网络反向传播以调整权重和偏差,旨在减少误差(图 2-13)。

图 2-13. 反向传播

/>

梯度下降

一种优化函数,尝试找到函数的最小值。另一种说法是,梯度下降的目标是尽可能地最小化成本函数(见图 2-14)。当这一目标达成时,网络被优化了。一个常见的类比是一个人走下山。他每走一步都希望朝向下降的方向前进,直到达到最低点;这就是成本函数最小化的地方。当这一目标达成时,模型的准确性最高。

图 2-14. 梯度下降

学*率(Learning rate)

学*率是我们为了实现梯度下降的最小值(山底)而采取的步骤大小。如果学*率太大,会超过最小值并潜在地失控。如果太小,过程会花费太长时间(见图 2-15)。

图 2-15. 学*率

神经元

神经元是神经网络的基础。它接收一个或多个输入,对这些输入应用一个函数,并生成一个输出。它在某种程度上基于人类神经元(见图 2-16)。

图 2-16. 神经元

函数

函数是神经元内的数学方程,它接收输入值并决定是否应该激活(或发出)。有许多激活函数,但这些是神经网络中最常见的:

Sigmoid

将输入值置于 0 到 1 的范围内(见图 2-17)。

图 2-17. Sigmoid

Tanh

将输入值置于-1 到 1 的范围内(见图 2-18)。

图 2-18. Tanh

ReLU

修正线性单元(Rectified Linear Unit)将输入值置于 0 到无穷大的范围内。它使所有负值为 0(见图 2-19)。

图 2-19. 渗漏修正线性单元

Leaky ReLU

将输入值置于非常小的负值到无穷大的范围内(见图 2-20)。

图 2-20. 渗漏修正线性单元

Softmax

接收输入并在一定的可能性集合上预测结果。例如,在数字识别中,Softmax 函数返回 10 个可能性(0-9),每个可能性都有概率。如果你有五种不同的汽水,它将返回五种可能性,每种可能性都有相应的概率(见图 2-21)。

图 2-21. Softmax 函数
注意

ReLU 函数存在“死亡”的问题——它会陷入负值侧并始终输出值为 0。使用具有轻微负斜率的渗漏修正线性单元或降低学*率可以解决这个问题。

作为业务分析师,我们建议对机器学*,特别是神经网络,采取高层次的视角。在这里,你可以深入研究 S 型或双曲正切函数的确切差异,或者如何确定梯度下降的确切方法。你可以深入数学领域,甚至可以撰写许多博士论文。我们在这个概述中的目标是向 SAP 业务分析师传达这门美妙科学的深度。此外,对这门科学的基本理解将使你能够利用它获得实际结果。

现在我们掌握了一些基本概念,那么今天我们实际上看到的一些基本神经网络是什么呢?

单层感知器

单层感知器 是神经网络的最简单形式(图 2-22)。它没有隐藏层。它只有一个输入和输出层。你可能认为图表有两层,但是输入层不被认为是一层,因为它不进行计算。单层感知器接收多个输入信号,对它们求和,如果值高于预定阈值就发射。因为它们只有值或没有,它们只能区分两个线性可分的类别。这有什么了不起的?单层感知器本身相当有限。然而,它们构成其他神经网络。想象一下:普通人的大脑有 1000 亿个神经元。每个神经元的功能很简单,就像这个单层感知器一样。正是我们大脑中这些神经元的合奏构成了我们的音乐。

图 2-22. 单层感知器

多层感知器

多层感知器 由多个层组成(图 2-23)。它们通常是互相连接的。第一个隐藏层中的节点连接到输入层中的节点。偏置节点可以添加在不连接输入层的隐藏层中。偏置节点增加了网络适应数据的灵活性,它们的值通常设为 1。在更高级的神经网络中,批量归一化 进行这个功能。

图 2-23. 多层感知器

卷积网络

卷积神经网络(CNN)是通过层间传递权重和偏置的多层网络。CNN 假设输入是图像,因此这些网络有特殊的层和编码。为什么不用多层感知器进行图像分类呢?嗯,图像数据很大……它不会很好地扩展。CNN 使用三维张量作为它们的输入,包括宽度、高度和深度。

图 2-24. 卷积神经网络层

CNN 有三个独特的层:

卷积层

主要目的是从输入中提取特征。每个图像都是像素值矩阵,通过滑动在图像上的滤波器计算点积来转换为特征。

池化层

有时也称为降采样或子采样。它通过使用最大值、平均值或总和值来减少由卷积层呈现的特征的维度。

全连接层

类似于使用 SoftMax 激活函数的多层感知器,将概率分布传递到输出层。

CNN 可以变得非常复杂。查看谷歌的 Inception 模型,如图 2-25 所示。

图 2-25. 谷歌的 Inception 模型

神经网络领域正在经历快速且令人兴奋的变化。杰出的思想家们正努力推动这一领域的快速发展。随着研究人员萨拉·萨布尔、尼古拉斯·弗罗斯特和杰弗里·辛顿提出了胶囊网络(Capsule Networks)的提议,这一变化尤为明显。(辛顿在这一领域是一个偶像级人物;只要他的名字出现在一篇论文上...你就会去读它。)在多层神经网络中,根据你的需求不断添加更多层次。在 CapsNet 中,你在另一层中添加一个神经网络。

正如辛顿所说,“卷积神经网络中使用的池化操作是一个大错误,它能够如此出色地工作,这是一场灾难。”

令胶囊网络如此令人兴奋的原因在于它们,就像我们自己的图像处理一样,不考虑图像的方向。当一个孩子看着一只狗时,狗的方向并不会影响他/她对图像的感知。

CapsNets 在目前仍然很新,但如果它们继续受到关注,我们将在未来的版本中更全面地讨论它们。

递归神经网络

递归神经网络是利用时间序列或顺序数据的多层网络。它们表现非常出色,通常是自然语言处理(NLP)任务和时间序列数据的首选模型。我们将在语言与文本处理章节中看到它们的运行。在其他神经网络中,一旦数据传递到下一层,前一层就被遗忘了。然而,在试图对数据序列进行预测时,记住之前发生过的事情是很重要的。这些网络是递归的,它们会回溯并查看之前的输入或输入。从某种意义上说,它们具有记忆功能。

环绕返回的箭头显示了 RNN 中的循环(见图 2-26)。正如你所见,这种循环非常短暂;它仅在同一层上循环返回。从本质上讲,它只有短期记忆。引入长短期记忆(LSTM)网络克服了这一问题。

LSTM 允许网络在长时间内学*。它们有三个门:输入门、输出门和遗忘门。输入门确定允许进入的数据。输出门确定允许输出的数据。最后,遗忘门决定应该遗忘哪些数据。它们的架构对初学者来说可能很难理解,但可以说 LSTM 允许网络在长时间内记住。如果您对它们更深入地了解感兴趣,请阅读这篇博客

图 2-26

图 2-26。前馈和递归网络

时间网络

时间卷积网络(TCN)是一个多层网络,具有卷积网络的优点,同时也考虑位置和摆放。

卷积网络通常非常擅长图像识别和语言分类。然而,它们并不关心位置。例如,卷积神经网络想知道图像中是否有尾巴、棕色的鼻子和垂耳。然后将该图像分类为狗。它并不关心图像的位置。在语言分类中,卷积神经网络想知道是否存在某些关键词,这些关键词会提示它是否在查看法律文件、漫画书或海明威的小说。同样,位置并不真正重要。但是如果您想处理位置和摆放很重要的数据,比如时间序列数据呢?时间序列数据就是一个带有日期和/或时间戳的时间轴上的数据集。正如我们之前提到的,处理这类任务的行业首选模型是循环神经网络(RNN)。然而,像许多数据科学中的事物一样,这个模型最*被强大的 TCN 取代了。

相较于 RNN,TCN 在计算上更加经济实惠,并且使用更简单的架构。RNN 需要资源,如 LSTM 层来记住。TCN 使用映射到下一层输入的输出步骤(参见图 2-27)。它们不使用递归,而是使用一个层的结果来馈送下一个层。

图 2-27

图 2-27。时间卷积网络

在第六章中,我们进行了一个简单的销售预测。TCN 似乎是用于此类任务的合适模型;我们将尝试使用它来预测从 SAP 系统中某个特定产品的销售情况。

自编码器

自编码器是一个仅具有前向传递的神经网络,其定义看似简单。它是一个接受输入数据并试图将其复制为输出的网络。它由两部分组成:

编码器

拆解输入数据。

解码器

重构数据以供输出。

这种类型的网络最常见的用途是图像去噪和图像生成。自编码器的真正价值不在于输出,这在我们其他神经网络的情况下是如此。真正的价值在于神经网络对压缩数据输出的表征。为了进一步澄清这一点,模型在最压缩的情况下已经学会了对象的显著特征。比如说,它正在观察一张狗的图片。显著特征包括耳朵、眼睛、嘴巴、吻、类似狗的鼻子等等。如果模型压缩得太远,可能会认为唯一显著的特征是眼睛,无法区分狗和其他动物。如果模型压缩得不够,识别了太多特征(比如颜色和面部形状),它只会知道一种类型的狗。这种模型的技巧在于知道平衡点。总结一下,神经网络的优化不是在输出接*输入时,而是在输出仍然代表输入的关键特征且数据尽可能压缩的时候。

注意

自编码器的一个关键概念是,输出维度必须小于输入维度,以便网络学*最相关的特征。

图 2-28. 自编码器

自编码器通常用于减少数据的维度和特征学*。它们通常是另一个神经网络的一部分,帮助减少特征的维度。

生成对抗网络

生成对抗网络(GAN)是一种神经网络架构,其中有两个网络,坦率地说,它们相互对抗。因此有对抗一词。这两个网络分别称为生成器和判别器。想象一下这种常见的场景。GAN 想制造假钱。生成器制作一张钞票并将其发送给判别器进行测试。嗯,判别器知道真钞看起来是什么样的,因为它已经从一组真实世界的图像中学*了。生成器的第一次尝试是可悲的,它失败了并且得到了失败的反馈。然后它再试一次,再试一次,直到能够生成一个让判别器认为是真实的钞票。然后轮到判别器学*了。它发现自己错了,并学会不再接受那张假钞票。这种争吵来回进行,直到网络在失败和成功上达到了一个平衡点......即不再有学*发生的点。

图 2-29. 生成对抗网络

/>

你可能会想知道这样的网络会如何使用。嗯,这些网络喜欢模仿数据。因此,它们已经被教会模仿艺术、音乐、图像,甚至诗歌。它们可以被教会合并图像中的概念。例如,你训练网络使用戴帽男士的图像,不戴帽女士的图像,并要求 GAN 生成戴帽女士的图像,它会做得相当不错。听起来很棒,但在我们的业务场景中有什么用呢?嗯,GAN 已被用于检测数据中的异常,也用于为其他网络生成训练数据,当数据量有限时。在这里我们提供的神经网络介绍中,我们不提到 GAN 是不完整的。但是,我们承认将它们应用于业务应用程序要困难得多。在此介绍它们只是为了说明我们在 SAP 业务分析师社区中创建一种公民数据科学家的目标。请记住,包括 GAN 在内的所有概念,也许你能找到一个能够应用 GAN 的业务场景。

摘要

如果这是你第一次接触数据科学概念,我们理解这可能有些难以接受。如果你是一位经验丰富的数据科学家,你可能会问:“XGBoost 在哪里?”或者“为什么不用 AutoML?”请记住我们的主要意图,我们希望业务分析师能多少像数据科学家思考一下。我们在本章中没有涵盖的数据科学的其他领域,如探索性数据分析和数据可视化,将在后面进行讨论。业务分析师们,我们希望你在本章中找到一些能让你思考你自己数据的想法,特别是本书中的 SAP 数据。在接下来的章节中,我们将使用 SAP 数据和我们在本章介绍的概念来进行详细的业务场景讨论。

¹ 作者建议在学*参与 F1 赛车之前先学会停车,但我们没有使用本书中的任何技术进行分析。那么谁知道呢?也许成为 F1 赛车手但不学会停车更好!需要更多数据来证明。

² 欧氏距离简单来说就是两点之间的普通直线距离,可以是平面上的或者三维空间中的。为什么说“直线距离”而不说“欧氏距离”听起来更学术呢?如果你有烟斗或羊毛衫夹克,额外加分。

³ 这种情况的更详细解释可以在http://yudkowsky.net/rational/bayes#content找到。

⁴ 图像分类是指从图像中提取信息并对其进行分类的过程;例如,识别一张图片是吉娃娃还是蓝莓松饼

第三章:SAP 数据科学家

注意

如果你是 SAP 专业人士,你可能不需要本章的大部分信息。我们正试图让数据科学家了解你可能已经知道的事情。

在 Big Bonanza Warehouse,Greg 和 Paul¹ 组成了整个数据科学团队。无论他们往哪里看,都会看到大量有趣的数据:工厂自动化系统、客户发货的运输记录、营销活动数据以及大型企业中随处可见的大量电子表格和 Microsoft Access 数据库。他们无法喝咖啡都听到另一个引人入胜的数据机会。他们既感到欣喜若狂又感到不堪重负:他们每天都要处理有趣的问题,但是他们永远也无法赶上堆积如山的数据请求。

当然,他们也有一种赶上来的方法。他们可以深入学* SAP。

因为 SAP 是继续吞并其他大型宝库系统的利维坦。随着 Big Bonanza 将其企业软件资源整合到 SAP 组合中,更多珍贵的数据消失在这头巨兽的腹中。Greg 和 Paul 知道 SAP 中数据量和机会之多令人难以置信。但他们不知道如何开始探索获取这些数据。与 SAP 最终用户交流无法揭示真正的数据模型,而与 SAP 管理员交流几乎毫无进展,因为他们的工作负荷实在太大。Greg 和 Paul 需要一种方法来进入这个领域。

数据科学家们,请注意。本章介绍了 SAP 基础知识,将帮助你达到你想要的目标。SAP 专业人员花了很多年时间熟悉 SAP 的各个方面。阅读一章书后,你不会像马哥伦那样导航。这是你成为与 SAP 团队和 SAP 数据合作的合格者的方法。

如果你听过“我知道的只够让我危险”的说法——这正是我们的目标。

开始学* SAP

SAP 是具有多个高度可配置功能的企业资源规划(ERP)软件。安装并运行 SAP 的企业可以选择多个可能的模块(参见 第一章 部分清单),但目前最常见的 SAP 版本是 R/3 ERP,这是一个在应用服务器上需要大量应用处理的客户端/服务器架构。

应用服务器几乎肯定由 SAP 社区内被称为 SAP Basis 管理员的一组管理员设置和维护。他们专注于安装和配置 SAP 系统,以实现高可用性运行。SAP Basis 管理员执行数据库管理、服务器管理、调度和故障处理批处理作业、管理低级安全性、修补应用程序和运行应用程序的基本操作系统等许多其他技术任务。

与 SAP 合作意味着您将使用功能强大(尽管看起来有些过时)的 SAP GUI。您很可能需要通过或从公司 IT 部门获得指示来安装它。通常由 Basis 团队维护的连接设置指向您的安装到正确服务器。一旦 GUI 安装完成,您可以通过打开 SAP GUI 登录板(图 3-1)来启动 SAP。

图 3-1. SAP GUI 登录板

如果您的 SAP 登录板上有许多系统连接,请不要担心,这是正常的。考虑到为许多 SAP R/3 应用程序安装了单独的系统,并且每个系统存在于沙盒、开发、QA、预生产和生产环境中,您可以看到:通常有数百个可能的系统可供登录。了解您需要的数据在所有这些系统中的位置可能需要您公司 SAP 专家的至少一个初始指引。

但一旦找到合适的系统并获得适当的访问权限,您就可以准备登录。请记住:就像任何企业系统一样,获取此访问权限通常需要通过安全专业团队的审核,并可能需要几秒到几周的时间。一旦完成并输入您的凭据,您将看到类似于图 3-2 的内容(尽管有些公司选择在 GUI 屏幕右侧放置其他图片)。

图 3-2. SAP Easy Access 屏幕,准备运行事务

现在您已经准备好进行一些真正的探索了。最初出现的屏幕允许您通过顶部的命令字段手动输入 t-code 启动任何 SAP GUI 功能,而左侧的所有文件和文件夹则是 t-code 的快捷方式。

将 t-code 理解为 SAP 内部的程序快捷方式。输入 t-code VA03,如图 3-3 所示,将启动 SAP 程序查看销售订单;输入 SE37 将进入 SAP 函数编辑器等。最终用户有通过网页界面或移动应用程序等其他方式查看其业务 t-code 的替代方法,但数据科学家需要探索 SAP 应用套件的管理侧面。

图 3-3. 显示和编辑销售订单的第一个屏幕

如您从图 3-3 中看到的 VA03 截图,SAP 终端用户体验可能变得非常混乱。典型的 SAP 用户通常只需使用订单的少量字段。在某些情况下,向通常不使用的字段输入数据可能会触发意外功能。

这正是使 SAP 和数据科学交汇如此奇怪却又如此有趣的部分。从 SAP 中可以获取到大量数据。您从哪里找到它?您甚至如何知道它在那里?

对于你们数据科学家,这本书将帮助你们在某些时间回答一些问题。对于你们 SAP 专家,放心,所有那些辛苦赢得的经验和领域知识对数据科学过程有着巨大的价值。

ABAP 数据字典

SAP 系统的一个重要功能是其内置的灵活性,允许客户添加或更改交付功能。这使得 SAP 能够交付支持包和增强功能,供客户自行实施,同时也允许客户在 SAP 系统内部创建自己的数据对象和程序。

SAP 系统为让客户定义自定义数据对象规定了一个过程。这些工具、对象和流程的集合通常称为ABAP 数据字典。数据科学家和其他程序员可能熟悉用于创建数据类型和表的常见 SQL 命令。SAP 在概念上提供了相同的功能,但实际上对其进行了加强。数据定义的变更立即影响系统的原始应用功能。例如,通过修改 SAP 表中字段的定义,您可以立即使屏幕为用户提供一个友好的下拉列表,用于输入字段的值。

我们将在这里介绍 ABAP 数据字典的一些主要部分,因为它们在解析您可以访问的系统中的数据时非常有用。

Tip

强大的功能伴随着巨大的责任!那些第一次窥探 SAP 的数据科学家们应该避免编辑 SAP 数据字典对象的诱惑。如果管理员给了您开发者访问权限,您可能会发现自己有权进行此操作。但请不要这样做!

更改数据字典对象可能会导致系统多个区域的不稳定性。作者曾见过一次不明智的数据字典更改导致整个 SAP 实例必须从备份中恢复。可以告诉你的是,最有经验的 SAP 老手也是最谨慎触碰数据字典对象的人。

Tables

在大多数 SAP 系统中,应用程序运行在像 Microsoft SQL Server、Oracle 或 IBM DB2 这样的传统 RDBMS 之上。与直接编辑这些数据库系统以进行更改不同,SAP 为用户提供了几个可以查看和更改系统数据库的事务代码。在过去几年中,SAP 推出了自己的 RDBMS,称为 HANA。它正在全球 SAP 环境中获得立足点,但截至目前尚未成为 SAP 数据库市场的主要股东。在本章的后面,我们将展示为 HANA 系统创建数据服务的示例。²

让我们深入研究一些 SAP 表。转到事务码 SE11,输入 VBAK 到数据库表字段中。点击显示按钮,表定义如图 3-4 所示。

Figure 3-4. 在事务码 SE11 中的 VBAK 表定义

VBAK 是一个透明表,这意味着屏幕上显示的字段和其他信息与底层的关系数据库管理系统中的表一一对应。这比在 SQL 中盯着某些 DDL 语句好得多。SAP 还允许另外两种类型的表:汇总和集群。对于本书的目的,我们不需要详细讨论,但要理解 SAP 汇总和集群表与关系数据库管理系统表不是一一对应的,因此屏幕显示的数据物理存储方式并不完全代表数据库中数据的物理存储方式。对于 SAP 中的大多数编程任务,这种区别并不太重要。³

注意

VBAK 是销售订单头表。VBAP 是销售订单行项目表。在查找 SAP 表时,请记住:SAP 是德国软件。K 通常代表“Kopf”或头部。P 通常代表“Position”或项目。

SAP GUI 列出了此表的字段在底部部分,包括它们的数据类型和长度。字段列给出了数据库中字段的名称——在我们的第一个示例行中是 MANDT。选中 Key 复选框的行必须在记录写入表之前以组合形式唯一,因此在我们的示例中,MANDT 和 VBELN 必须唯一。数据元素列标识引用数据对象类型,进一步由数据类型、长度和小数点列指定。对于试图确定字段用途的数据科学家,简短描述列提供了这一点。如果你滚动到字段列表底部,可能会看到一个或多个字段名称带有 Y 或 Z 前缀。这表示公司已向交付的数据库表中添加了字段,因为数据科学家可能需要在 SAP 系统中收集其他唯一数据。⁴ SAP 使用 Y 或 Z 来命名这些字段。但是,这些新字段确实在添加时成为底层数据库表的一部分。

T-code SE11 在搜索 SAP 数据时提供了另一个有价值的功能。顶部附*有一个“Indexes…”按钮。这让你知道哪些字段被优化用于搜索此表,这在考虑某些较大的 SAP 表时非常重要。索引是灵活性的另一个关键点——SAP 应用程序允许你向基本系统表中添加自己的索引以提高搜索性能。然而,索引并不完美。它们可能会造成性能成本,因此在向 SAP 表添加自己的索引时要谨慎和实用。

如果我们勇敢的数据科学家 Greg 和 Paul 需要检查特定的销售订单集以查看系统中存在的数据类型,他们可以使用 t-code SE16(通用表显示)或较新的 SE16N,如图 3-5 所示。知道 VBAK 是订单头表的提示来自他们的 SAP 同事,他们可以在下部分输入搜索条件,并查看结果。请注意,他们可以限制返回的记录数量,这对于发现他们是否在等待 3000 万行返回到屏幕上以确定他们是否正在搜索正确的内容非常有用。SE16/SE16N 更适合查看表中的数据,而 SE11 更适合检查表的设置详细信息。

图 3-5. 在 SE16N 中显示的 VBAK 表,用于查询

结构

SE11 还用于定义 SAP 结构。结构是在字典中定义的字段组,提供了在 SAP ABAP 代码中引用数据的常用方式。在“OData 服务”中,我们使用字典定义的结构来保存 SAP 物理工厂数据。如果像图 3-6 中那样在字典中定义了一个结构,那么整个 SAP 系统中的程序都可以使用该结构来创建内部变量。结构本身并不永久保留数据;它们只是用于生成数据的模板。

图 3-6. 在 t-code SE11 中的结构视图

视觉效果非常类似于表视图。您可以查看字段名称、数据类型和一些基本描述。作为数据科学家,您可能不会经常定义新的结构,但好处是定义自己的结构比重新定义基本应用表的风险要小得多。例如,在第五章中,您将了解到异常检测,我们将定义自己的结构以用于从 SAP 提取数据。

数据元素和域

SAP 应用层使用数据元素作为结构和表定义(除了像整数和字符串这样的基本类型之外)的最低级别部分。它们可以在整个系统中的结构、表和程序中使用,这使它们成为定制字典定义中灵活性的关键部分。

数据元素也在 SE11 中定义(见图 3-7)。

在这里,您将数据元素定义为预定义的基本类型或域的一部分。在图 3-8 中显示的屏幕截图中,SAP 提供的 LOGSYS 数据元素使用了同名的 LOGSYS 域。这是安装在基本系统中的。尽可能使用域来赋予您的数据元素一些额外的功能。

图 3-7. 在 t-code SE11 中定义的数据元素

图 3-8. LOGSYS 的域定义(在本例中,LOGSYS 是 LOGSYS 数据元素的域)

域可以作为数据元素之上的一种感性层。最强大的是,您可以为该域定义一个可接受的值列表,无论是通过硬编码列表(如 VBTYP 域的图 3-9 所示),还是指定一个 SAP 表作为该域可能值的定义(如 LOGSYS 域的图 3-10 所示)。

图 3-9. VBTYP 域的硬编码可接受值列表

在 SAP 中进行信息搜索时,使用域定义的数据元素可以极大地帮助查找可能的值。了解可能的值及其含义会立即提升您对正在查看的数据的洞察力。例如,如果 Greg 和 Paul 从图 3-9 中看到的 VBTYP 字段提取信息,数据将从 SAP 中提取出这些 A、B、C……等快捷值。Greg 和 Paul 可以通过参考 VBTYP 的域定义快速转换这些值。

图 3-10. 在 SE11 中配置的 TBDLS 表中定义的 LOGSYS 域的可能值

使用位置

我们已经讨论了在 SAP 系统中定义数据的关键点,包括表、结构、数据元素和域。在您探索和寻找正确信息集的过程中,一个最强大的工具是“使用位置”清单,适用于所有这些元素。带有箭头的小奇怪方框(图 3-11 所示)表示您可以四处寻找相关信息。

图 3-11. “使用位置”按钮,用于查找对您正在查看的 ABAP 对象的引用

例如,Greg 和 Paul 得知表 VBAK 是销售订单头表。他们怀疑其他系统表中也包含有价值的相关信息,但不知道在哪里找到它们。他们阅读字段的简短描述,并决定进一步深入研究 VBELN。

要查看如何执行此操作,请单击以突出显示 VBELN 字段,然后如图 3-12 所示双击数据元素 VBELN_VA。在数据元素屏幕上,单击“使用位置”图标。从出现的对话框中选择“表字段”,如图 3-13 所示,完成!将显示包含 VBELN 字段的其他表的列表,如图 3-14 所示。然后,通过在 t-code SE16 中进行一般表显示和查找相关数据的结合分析,您可以从可用表数据中组合出您正在寻找的模型。

图 3-12. 选择数据元素 VBELN_VA 进行“使用位置”分析

图 3-13. 限制“使用位置”搜索仅限表字段

图 3-14. VBELN_VA 在表中使用位置的部分列表

个体域和数据元素也是如此。如果您的调查已经让您发现一个您认为对数据调查至关重要的数据元素,您可以“使用它”,找出系统在哪些其他地方使用了这个字段。通过这种方式,您可以找到使用该数据元素的表格;希望这个表格包含您需要的数据。

除了揭示 SAP 表的模型结构外,您还经常可以发现支持您搜索的程序元素。通过搜索函数、类和其他程序对象来查找您选择的字典对象,您可以访问可能的实用函数和可重复使用的代码。在本书的几个后续章节中,我们将使用 SAP 函数和表来展示这些关系,但教授您如何编写 SAP ABAP 代码并非本书的主要目标。⁵

ABAP QuickViewer

大多数情况下,您可能需要查看来自多个表格的数据。例如,您可能希望查看采购订单数据——但像 SAP 中的大多数数据一样,它按照头和项目进行拆分。如果您使用 SE16 或 SE16N,您需要从 EKKO 和 EKPO⁶ 查询数据——这并不是最有效的方法。为了进一步复杂化事情,每个表格上有数百个字段——其中绝大多数您都不需要。

这里有一个查看多个相关表的更好工具:ABAP Quick Viewer。此工具允许您创建快速查询,您可以在 SAP GUI 中查看或提取到另一个工具中。让我们看一个快速的例子。

在命令栏中输入事务码**SQVI**。您将看到与图 3-15 相同的 Quick Viewer 初始屏幕。输入查询的名称,采购,并点击“创建”按钮。

图 3-15. QuickViewer 初始屏幕

输入标题并将数据源更改为表连接。点击“输入”按钮(绿色圆圈内有勾号的按钮)。

您将看到一个空白画布,您可以在其中查看您使用的表格。点击“插入表格”按钮(图 3-16)。

图 3-16. 插入表格按钮

在“添加表格”对话框(图 3-17)中,输入查询的主表 EKKO。点击“输入”按钮。

图 3-17. 向 QuickViewer 设置中添加表格

现在您会在画布上看到您选择的主表。再次点击“插入表格”按钮以添加另一个表格——EKPO。再次点击“输入”按钮以接受它。

SAP 试图检测两个表之间的正确关系,并在画布上显示结果;然而,它并不总是完全正确。图 3-18 显示 EKKO 和 EKPO 之间存在两种关系——具体来说,EKKO-EBELN 到 EKPO-EBELN 和 EKKO-LPONR 到 EKPO-EBELP。第二个关系(对于我们的目的)不正确,需要删除。

图 3-18. EKKO 和 EKPO 之间的两个自动建议关系

右键单击要删除的连接,并从上下文菜单中选择“删除链接”。

图 3-19. 链接的上下文菜单

现在关系正确了,点击“返回”按钮,会看到一个类似图 3-20 的屏幕,并选择字段。右侧是可用字段的列表。有数百个字段,但我们只需要几个。点击所需字段,然后点击“添加字段”按钮。

图 3-20. QuickViewer 字段选择

选择的字段(图 3-21)将移动到左侧面板。

图 3-21. 在 QuickViewer 中选择的字段

点击“选择字段”选项卡(如图 3-21 所示),并选择要查询数据的字段(图 3-22)。例如,我们只想按日期筛选我们的数据,因此我们将选择“记录创建日期”并将该值移动到左侧,就像我们之前做的那样。

图 3-22. 选择显示的字段

就这样!查询准备就绪!点击“保存”按钮保存您的工作。接下来点击“执行”按钮进行测试。⁷ 您将看到一个简单的报告,只包含我们指定的选择标准。输入采购订单的日期范围,然后点击“执行”按钮。SAP 将显示您请求的采购详细信息的简单表格报告(如图 3-23)。

图 3-23. QuickViewer 查询结果

此工具栏在整个 SAP 中都可以找到,并且几乎所有标准报表都包含在内。

特别注意导出按钮。当您点击它时,会显示一些导出数据的选项(图 3-24)。选择“本地文件”将带您到本地文件选项(图 3-25)。

图 3-24. 查询导出选项

图 3-25. 本地文件选项

作为数据科学家,您已经知道如何将制表符分隔文件导入到 R、Python 或 Weka 中。ABAP Quick Viewer 提供了一种简便的方法来进行一些初始数据导出和关键的探索性数据分析。通过使用这种技术,您可以快速地从 SAP 获取数据,并进行概念验证,而不需要太多的努力。

SE16 导出

现在您可以在 SAP 中看到数据,想要在您选择的工具中进行一些探索性数据分析和可视化。也许您计划使用 R 和 R Studio 或 Jupyter Notebook。甚至是 Weka!让我们从 SAP GUI 中提取这些表格数据。

通用表显示 t-code SE16 带有这个方便的工具栏(图 3-26)。

图 3-26. SE16 通用表显示工具栏选项

数据科学家感兴趣的两个按钮:“在 Excel 中打开”(左圆圈)和“本地文件”(右圆圈)。它们的功能都符合您的预期。在导出之前,“本地文件”按钮提供格式化选项。

有权访问原始数据文件的数据科学家是一位快乐的数据科学家。

OData 服务

现在您已经了解了如何找到正确的数据并手动提取它,让我们看看如何再现该数据提取并使其外部可用。回想一下第一章提到的,OData 服务在数据提取方面有其优势。

  • 它们符合流行的 REST 范式。

  • 它们将数据服务暴露给任何能够进行 HTTP 调用的设备/客户端。

  • 它们允许通过结果数据进行过滤和分页。

现在我们知道如何定义数据字典对象,我们可以利用它来构建一个简单的 OData 服务的组件,使用 SAP NetWeaver Gateway。在我们开始之前,有几件事情需要记住:

Gateway 架构

一些 SAP 安装使用单独的系统来运行 Gateway 组件,而其他一些则将 Gateway 安装在与所有 ERP 模块相同的系统中。为简单起见,我们假设我们在与所有模块相同的系统上,并且只在可能存在差异时进行注释。

ABAP 编程

我们将尽量减少我们示例中编写的代码量。正如前面提到的,本书不是 ABAP 入门指南。希望各位数据科学家在不必花费大量时间学*语言的情况下能够获得所需的数据。

安全问题

默认情况下,大多数运行 NetWeaver Gateway 的公司将其部署在企业防火墙之后。您可能需要一台运行在企业网络内部的计算机来使用您创建的 OData 服务。虽然可以将其暴露给更广泛的互联网,但请务必听取安全团队的建议,找到最佳的安全和监控 API 的方法。

其他 OData 功能

OData 还允许完整的 RESTful 操作集(创建/读取/更新/删除),只要它们被编程实现。我们将仅利用和解释读取能力,因为它们对本书最相关,但如果您的场景扩展到其他数据收集和分析,OData 是创建这些能力的绝佳起点。

由于我们将在本书中的其他用例中使用 OData,让我们做一个简单的示例来展示设置的方法。我们将创建一个简单的服务,返回配置在 SAP ERP 系统中的所有工厂的列表。

首先,我们将创建一个结构来定义我们将提供的数据形状。在 t-code SE11 中,点击“数据类型”旁边的单选按钮。在其旁边的字段中输入 **ZEXAMPLE_PLANT**,然后点击“创建”按钮,如图 3-27 所示。

图 3-27. 在 SE11 中定义一个新结构

当您点击创建时,将会看到三种类型之一的选项:数据元素、结构和表类型。选择结构如同图 3-28 所示,并点击勾选继续。

图 3-28. 结构类型选择

在下一个屏幕(图 3-29)上,您定义实际进入结构的字段。保持简单,仅定义植物和描述字段。完成后,点击魔术棒按钮激活此结构。这将使该结构可供外部程序使用。请注意,您需要在顶部部分输入结构本身的描述。

图 3-29. 我们新的 ZEXAMPLE_PLANT 结构定义的 Plant 和 Description 字段

我们如何知道要为此结构使用什么类型和数据元素?我们发现植物存储在销售订单的行项目级别中,存储在表 VBAP 中。使用 SE11 查看该表,我们找到了植物字段(WERKS)及其关联的数据元素(WERKS_EXT)。通过点击数据元素,我们找到定义 WERKS_EXT 的域为 WERKS。我们使用该域的属性来发现其底层的表:T001W(图 3-30)。然后,在其自己的 SE11 会话中打开 T001W,我们发现 WERKS 和 NAME1 字段包含我们想要的信息。就像我们为数据字典的内容所展示的一样!因此,对于我们的两个字段,PLANT 和 DESCRIPTION,我们使用与 T001W 中相应字段相同的数据元素。

图 3-30. 在 SE11 中查看的定义植物和分支的 T001W 表

现在我们已经有了返回数据的正确形状,让我们设置 OData 服务来提供它。输入事务码 SEGW。这就是网关服务生成器(图 3-31),您设置和维护 OData 服务的一站式工具。点击小白纸按钮开始构建我们的服务。

图 3-31. 网关服务生成器

在弹出窗口中,按照图 3-32 中显示的值填入。确保用你的实际用户 ID 替换*[YOUR_USER]*(默认应已填充),然后点击勾选。您将看到服务的骨架已创建。现在我们可以插入我们创建的结构,并自动将其作为服务的一部分。

图 3-32. 命名和定义 SEGW 项目

OData 服务可以有多个数据源插入它们。这些数据源称为实体实体集。将实体视为定义数据源端点单记录结构的对象,将实体集视为匹配该结构的记录集合。一个服务可以附加多个实体和实体集,并且每个实体可以选择是否实现一些或所有的创建/读取/更新/删除操作。我们将把我们的植物信息转换为服务中可用的实体集之一,并且只实现读取功能。

右键单击数据模型文件夹,选择“导入...DDIC 结构”(DDIC 表示“数据字典”),如图 3-33 所示。

在向导的第一步(图 3-34),将**Plant**作为实体的名称输入,将**ZEXAMPLE_PLANT**作为 ABAP 结构输入(记住我们刚刚创建的结构),并确保选择创建默认实体集。

图 3-33. 导入 DDIC 结构

图 3-34. OData 设置向导步骤 1

在第二步(图 3-35),从结构中作为导入的所有可用字段进行检查,并点击“下一步”按钮。

图 3-35. OData 设置向导步骤 2

在最后一步(图 3-36),将 Plant 字段标记为关键字段,因为这将是这些记录的唯一识别信息。点击“完成”。

我们刚刚所做的确保了我们创建的结构被导入到服务中,作为 Plant 实体的定义。SAP 系统使用结构中的信息来确保 OData 服务被正确地类型定义。

接下来,我们生成一些数据提取类。SAP Gateway 使用生成的类来处理调用特定操作(创建/读取/更新/删除)时 OData 服务的默认行为,并且开发人员可以使用这些生成的类作为实现自己独特代码和功能的钩子。要生成这些类,请点击小棋盘状圆圈图标(在图 3-37 中标出)。

图 3-36. OData 设置向导最终步骤

图 3-37. 生成基本 OData 类

当完成这一步后,我们准备编写一些简短的数据检索代码。打开服务实现文件夹并展开 PlantSet 项目。右键单击 GetEntitySet(Query)并选择转到 ABAP 工作台(图 3-38)。您将收到一个听起来很恶心的信息弹出窗口,指示某个方法尚未实现。这没关系,这正是我们要做的事情!

图 3-38. 转到 ABAP 工作台

您将进入类生成器屏幕。使用屏幕左侧的导航,转到继承方法 PLANTSET_GET_ENTITYSET。右键单击它,并选择重新定义,如图 3-39 所示。

Figure 3-39. 准备重定义 OData ABAP 代码

在屏幕右侧,您将看到一个文本编辑器,允许您编辑方法的代码。我们这里不会深入讨论 ABAP 编程,所以请相信我们,并输入以下代码,然后点击魔术棒以激活代码:

| METHOD plantset_get_entityset.

SELECT werks AS plant

name1 AS description

INTO CORRESPONDING FIELDS OF TABLE et_entityset

FROM t001w

ORDER BY werks ASCENDING.

ENDMETHOD. |

通过输入此代码,我们已经做了足够的工作来设置服务以具备可运行的代码。现在我们需要做一些额外的步骤,使其作为 web 服务正常运行。转到事务码 /N/IWFND/MAINT_SERVICE 并点击目录顶部的添加服务按钮。下一个屏幕将允许您搜索,您可能需要与本地 SAP 专家沟通,以了解您的系统环境,以确定您是否需要使用本地系统作为网关,还是您有一个单独的网关服务器。如果您需要使用本地系统,则将输入一个系统别名如 LOCAL 或类似的,而如果您需要使用网关中心系统,则需要找到正确的别名。

在输入别名后,点击获取服务,并向下滚动找到我们的服务:ZEXAMPLE_PLANT_SRV。点击服务以查看单屏激活向导 —— 只需接受其默认设置,并返回到主服务目录。现在您的新服务将出现在主目录中,准备好进行测试。

在列表中向下滚动以找到 ZEXAMPLE_PLANT_SRV,并点击它。在屏幕底部,您将看到一个“ICF 节点”部分展开,如 Figure 3-40 中所示。点击该部分中的 Gateway 客户端按钮,即可进入 SAP GUI 网关测试工具。

Figure 3-40. 导航到网关客户端

将请求 URI 更改为 /sap/opu/odata/sap/ZEXAMPLE_PLANT_SRV/PlantSet?$format=json 并点击执行。您将看到作为服务调用结果的 JSON 格式化数据,如 Figure 3-41 中所示。就这样!您已经设置了一个简单的、可通过 web 调用的服务,用于获取 SAP 植物数据。我们在后面的章节中设置的 OData 服务会更加复杂,但这个初始过程会让您顺利起步。

Figure 3-41. 我们示例服务的 OData 请求结果

核心数据服务

正如本章前面承诺的那样,这里是使存储在 HANA 中的 SAP 数据可用的一瞥。请记住,HANA 是最新 SAP ERP 系统运行在其上的新后端数据库。如果您的系统是新的(或最*更新的),您可能已经准备好使用此功能。如果您是数据科学爱好者,请与您的 SAP 同事联系。

核心数据服务(CDS)是 SAP 的一个新特性,用户可以通过 HTTP 开发可以向客户端请求公开的数据模型。这些模型可以是表、SQL 视图、关联和用户定义的结构。可以将它们看作是 NetWeaver Gateway……但没有网关。它们是一种极其强大和有用的暴露、建模和分析 SAP 数据的方式。它们的功能是 SAP 店升级到 HANA 的最具诱惑力的原因之一。

虽然 CDS 有很多功能,我们将以一个简单的销售订单数据提取示例为例。为了创建这些视图,您需要安装并修改 Eclipse,以便它可以与 SAP 一起使用。大多数 ABAP 开发人员已经将其开发需求迁移到 Eclipse,但这是一个选择而不是必需的。CDS 视图需要 Eclipse。

www.eclipse.org 下载最新版本的 Eclipse,并按照向导说明安装。您至少需要安装Java 开发人员的 Eclipse IDE

安装完成后,您需要添加一些附加组件以使其与 SAP 正常工作。启动 Eclipse 并导航到菜单路径菜单 → 安装新软件,如 图 3-42。

图 3-42. 向 Eclipse 安装新的插件

在显示的对话框中(图 3-43),输入 URL https://tools.hana.ondemand.com/photon。用您使用的 Eclipse 版本替换photon

图 3-43. 选择要下载插件的 Eclipse 版本

按 Enter 键查看安装选项。然后选择您想要安装的组件。在我们的示例中,我们选择了 图 3-44 中的所有 SAP 选项。

图 3-44. 安装到 Eclipse 的软件选择

点击“下一步”按钮等待组件安装完成。完成后,它将显示添加到 Eclipse 环境中的所有软件组件。再次点击“下一步”接受许可协议,然后点击“完成”按钮。

安装完成后需要重新启动 Eclipse。重新启动后,点击“打开透视图”按钮打开新的 SAP 环境。选择 ABAP 透视图如 图 3-45,然后点击“打开”按钮。

图 3-45. 选择 ABAP 透视图以使用正确的 SAP 开发设置打开 Eclipse

第一步是创建一个 CDS 文档。这些是设计时源文件,包含描述模型的 DDL(数据定义语言)代码。

在 Eclipse 中,按照菜单路径文件 → 新建 → 其他(图 3-46)。

图 3-46. 选择一个新的项目用于 CDS 文档

在对话框中展开 ABAP 和 Core Data Services,并选择数据定义选项(图 3-47)。

单击“下一步”按钮并接受默认项目。

为开发对象输入一个包。对于我们的目的,我们将使用 $TMP,这是 SAP 用于本地/不可传输对象的指定。

为您的服务输入名称和描述,然后单击“完成”按钮(图 3-48)。

图 3-47. 新的 ABAP 存储库对象用于 CDS

图 3-48. 完成新的 CDS 数据定义

在工作空间中有一些默认注解。我们将更改一些并添加一些新的。

默认情况下,注解从此处开始:

@AbapCatalog.sqlViewName: 'sql_view_name'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Data Definition for Sales Orders'

更改sqlViewNameauthorizationCheck

@AbapCatalog.sqlViewName: 'Sales_Orders'
@AccessControl.authorizationCheck: #NOT_REQUIRED

需要两个新的注解:

@VDM.viewType: #CONSUMPTION

表明我们要消费此数据定义。

@OData.publish: true

表明我们希望定义能够自动发布。

现在注解部分应如下所示:

@AbapCatalog.sqlViewName: 'Sales_Orders'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Data Definition for Sales Orders'
@VDM.viewType: #CONSUMPTION
@OData.publish: true

现在您可以设置数据的形状和关系。首先,定义要读取的主表:

define view YBD_DD_SALES
  as select from vbak as header 

如果有其他与主表关联的表格,请识别并命名它们:

    association[0..*] to vbap as line 
      on header.vbeln = line.vbeln 

确定并命名要从 SAP 系统中提取的字段。请注意,我们已包括一个计算行,用于显示即时输出的NetPrice

{
  key header.vbeln as SalesDocument,
  key line.posnr as SalesDocumentLine,
  header.erdat as CreateDate,
  header.erzet as CreateTime,
  header.vbtyp as DocumentCategory,
  header.auart as DocumentType,
  header.kunnr as Customer,
  line.matnr as Material,
  @Semantics.quantity.unitOfMeasure: 'UoM'
  line.kwmeng as Quantity,
  line.meins as UoM,
  line.kdmat as CustomerMaterial,
  line.pstyv as ItemCategory,
  round(line.netpr * line.kwmeng,2) as NetPrice
}

在选择中添加任何条件:

where header.auart = 'ZOR'

完整的最终定义:

@AbapCatalog.sqlViewName: 'Sales_Orders'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Data Definition for Sales Orders'
@VDM.viewType: #CONSUMPTION
@OData.publish: true

define view YBD_DD_SALES
  as select from vbak as header
    association[0..*] to vbap as line
      on header.vbeln = line.vbeln

{
  key header.vbeln as SalesDocument,
  key line.posnr as SalesDocumentLine,
  header.erdat as CreateDate,
  header.erzet as CreateTime,
  header.vbtyp as DocumentCategory,
  header.auart as DocumentType,
  header.kunnr as Customer,
  line.matnr as Material,
  @Semantics.quantity.unitOfMeasure: 'UoM'
  line.kwmeng as Quantity,
  line.meins as UoM,
  line.kdmat as CustomerMaterial,
  line.pstyv as ItemCategory,
  round(line.netpr * line.kwmeng,2) as NetPrice
}

where header.auart = 'ZOR'

单击“保存”按钮,然后单击“激活”按钮以发布数据定义。

通过按下 F8 键来测试服务。结果将显示在另一个标签页中(图 3-49)。

图 3-49. Eclipse 中的 CDS 数据结果

服务应该正确激活。如果您在 OData 注解旁边收到以下警告,则服务实际上没有发布。以下步骤允许您手动执行此操作。

在 Eclipse 中,点击 SAP GUI 按钮(图 3-50)。选择要启动的项目,然后点击“确定”按钮。这与创建数据定义的同一项目。

图 3-50. 使用 CDS 项目启动 SAP GUI

在事务字段中,输入事务代码**/n/iwfnd/maint_service**(图 3-51)。

图 3-51. 输入维护服务的事务代码

点击“输入”按钮(圈出),然后点击“添加服务”按钮。

输入系统别名,对于我们的情况将是 LOCAL,并输入技术服务名称,如 图 3-52。这是数据定义的名称,附加了“_CDS”。然后点击“获取服务”按钮。

图 3-52. 启动技术服务的设置

服务定义将显示在报告中。突出显示适当的服务,并单击“添加所选服务”按钮。

图 3-53. 将新服务添加到 Gateway 后端服务

最后,"添加服务" 屏幕显示如 图 3-54 所示。接受默认设置并添加包分配,我们的情况下是 $TMP。

图 3-54. 接受默认设置并添加 $TMP 包分配以将服务添加到网关

单击输入按钮(环绕)。如果一切操作正确,你将看到在 图 3-55 中显示的消息。

图 3-55. 成功创建 CDS OData 服务

返回具有 CDS 注释的数据定义屏幕,然后单击激活按钮(图 3-56)。

图 3-56. 激活按钮允许你使你的 CDS 定义在 SAP 系统中可用

在 OData 注释旁边有一个新的 生成的 OData 服务 指示器。将光标放在该符号上,将显示一个弹出窗口,显示 OData 服务的详细信息。单击类似 图 3-57 中的 OData 服务突出显示链接,可以在浏览器中查看 OData 服务定义。

图 3-57. OData 服务发布结果

概要

格雷格和保罗详尽地查阅了数据字典、数据导出、OData 和 CDS 信息,他们觉得已经准备好创建他们的 SAP 数据科学故事了!

与 SAP 数据一起工作的数据科学家永远不应忘记他们可以利用的真实资源。在企业中工作的 SAP 团队充满了深入了解数据模型的人。他们还具有极富帮助性的部落知识,了解他们所在企业如何定制 SAP 以适应其业务需求。就像黑客在尝试破解复杂加密之前从人们那里社会工程获取密码一样,与 SAP 工作的数据科学家应该在尝试逆向工程 SAP 数据模型的所有细节之前向那些有经验的 SAP 人士寻求知识。按照我们在这里描述的方式检查数据字典最终将帮助你找到需要的业务答案所需的数据,但这需要相当长的时间。坐在你身后的人很可能能轻易记住那些怪诞的 SAP 表名。

¹ 请允许我们这一次作者的自豪。

² 就因为我们是好人。

³ 在最新的 SAP HANA 发布版本中,汇集和集群表已经取消汇集和取消集群化。因此,如果你不理解它们,不要费心 — 它们即将消失。

⁴ 我们在章节开头的注释是认真的 — 只有当你是 SAP 系统管理员、有系统备份并完成必要的仪式时才能执行此操作。

⁵ 这很好,因为它不像 Python 或 R 那样有趣。

EKKO 是采购订单头表,而 EKPO 则是用于行项目。注意前面提到的 “K” 和 “P”?

⁷ 此时可能会看到一个日志详细信息对话框。只需点击继续。

第四章:R 的探索性数据分析

Pat 是大富翁仓库采购部门的经理。他的部门专门生产各种建筑行业的管子,这需要采购大量的原材料和半成品。然而,Pat 有一个问题;他每天在 SAP 系统中收到多达一百份采购申请,需要经过批准才能成为采购订单。这是一个繁重且耗时的过程,他希望能够得到帮助来优化流程。他决定向 IT 部门和 SAP 团队寻求帮助。

SAP 团队已经对系统进行了优化,使采购申请流程变得更加高效。当 Pat 和 SAP 团队联系他们的数据科学团队时,他们立刻想到:“我们能否建立一个模型来学*采购申请是否会被批准?” SAP 系统中有大量数据——* 10 年的历史数据——他们知道所有的采购申请批准和拒绝情况。原来有数百万条带标签的数据记录,所有这些记录都指示被批准或被拒绝。这不就是监督学*吗?当然是!

我们在第二章介绍了四种不同类型的学*模型。它们分别是:

  • 监督学*

  • 无监督学*

  • 半监督学*

  • 强化学*

我们倾向于认为这里提到的场景是监督学*,因为我们有带标签的数据。也就是说,我们有已经批准和被拒绝的采购申请。我们可以在这些带标签的数据上训练一个模型,因此这是一个监督学*的场景。确定了我们正在使用的学*模型的类型之后,下一步是探索数据。

数据科学家工作流程中最关键的过程之一是探索性数据分析(EDA)。数据科学家使用这个过程来探索数据,并确定是否能够对其进行建模,以及如何进行建模。EDA 的目标是通过总结主要特征来了解数据,通常使用可视化来实现。这一步要求数据科学家熟悉数据。

熟悉 SAP 系统的读者:如果您认为您已经熟悉了您的数据,请尝试进行这项练*。您会惊讶地发现自己学到了多少东西。对于了解关系型数据的一般形式和了解清理、分析和完全建模的结果之间存在着巨大的差异。

在本章中,我们将详细介绍探索性数据分析(EDA)的过程。为了使其更易理解,我们将实时进行。也就是说,我们不会操纵数据以便更轻松地编写本课程;相反,我们将尽可能真实和贴*实际地进行。在我们的情景中,我们将遇到问题,并像处理真实情景一样解决它们。如图 4-1 所示,EDA 包括四个主要阶段:收集、清洗、分析和建模。在深入探讨我们的情景之前,让我们简要介绍每个阶段。

探索性数据分析工作流程

图 4-1. 探索性数据分析工作流程

EDA 的四个阶段

收集数据阶段,我们从源系统的数据开始。了解源系统如何记录数据是很重要的。例如,如果我们不知道 SAP 表中的采购申请是什么样的,我们就无法将其提取出来进行后续分析。

一旦我们理解了源数据,我们选择方法和工具来提取并检查数据。在本章中,我们使用了从 SAP 中的平面文件提取作为中间存储,使用 R 数据分析语言来处理和分析数据。在专注于业务场景的 EDA 中,迅速迭代假设是非常重要的。因此,请选择您熟悉和*惯的工具。

如果您对任何工具还不熟悉,不要担心!有许多选项可供提取和分析。第三章 讨论了几种替代的 SAP 数据提取方法,本书的后续章节使用了其中的许多方法。R 语言在统计学家和数据科学家中很受欢迎,但 Python 也有一个非常强大的社区。在本书中,我们将使用这两种语言的示例和工具。

成功提取数据后,我们进入清洗数据阶段。源系统的数据库、数据维护规则以及我们选择的提取方法都可能在数据上留下自己的独特痕迹。例如,有时 CSV 提取可能会有额外的不需要的标题行。有时 API 提取可能会以不兼容分析工具的方式格式化数字。经常出现的情况是,当我们提取多年数据时,源系统的数据管理规则已经发生了变化。

当我们在提取后立即清洗数据时,我们寻找那些明显错误或不一致的事物。在本章中,我们使用 R 方法来清洗数据,而您可能会更喜欢其他语言。无论您的方法是什么,我们在这个阶段的目标是清除数据中明显存在的问题。

达到去除这些不良元素的目标后,是时候进入分析阶段了。这是我们开始建立假设并探索问题的地方。由于数据在经过清洗后可以信任,我们可以可视化关系并确定哪些关系最强大且值得进一步建模。

在这个阶段,我们经常会发现自己在重塑和重新格式化数据。这是一种清理数据的形式,重点不是去除不良(或格式不佳)的数据,而是专注于将好的数据塑造成下一个阶段可以有效使用的形式。分析阶段通常会为进一步重塑提供几个机会。

最后一个阶段是建模。到了这个阶段,我们已经在数据中发现了几个值得追求的关系。我们的目标是:创建一个模型,使我们能够得出见解深刻的结论或进行基于证据的预测。该模型应该是可靠且可重复的。通过对此采购场景进行建模,SAP 团队旨在为采购经理帕特提供具有深刻影响的信息和工具。

格雷格和保罗对这个流程非常熟悉,所以让我们开始吧!

第一阶段:收集我们的数据

从 SAP 获取数据的简便方法是使用 ABAP QuickViewer。此事务允许用户查看表格或连接在一起的表格集合的字段。对于采购申请到采购订单的情景,我们需要两个表格:采购申请的 EBAN 和采购订单行的 EKPO。使用事务代码 SQVI 启动 QuickViewer 事务。

输入快速查看的名称(如图 4-2](#quick_view_first_screen))。

快速查看第一个屏幕

图 4-2. 快速查看第一个屏幕

点击“创建”按钮并给快速视图命名一个标题(见图 4-3](#quick_view_title))。

快速查看标题

图 4-3. 快速视图标题

将“数据源”更改为“表连接”(如图 4-4](#quick_view_type_options))。

快速查看类型选项

图 4-4. 快速查看类型选项

点击“输入”按钮,然后点击“插入表格”按钮(如图 4-5](#fquick_view_create_table))。

快速查看创建表格

图 4-5. 快速视图插入表格按钮

输入第一个表格的名称,然后点击“输入”(如图 4-6](#first_quick_view_table))。

第一个快速查看表格

图 4-6. 第一个快速查看表格

重复此过程,点击“插入表格”按钮,然后点击“输入”(如图 4-7](#second_quick_view_table))。

第二个快速查看表格

图 4-7. 第二个快速查看表格

表格将显示在屏幕上,其默认关系已确定(见图 4-8](#quick_view_default_join_properties))。始终检查这些关系,确保它们符合要求。在这种情况下,确定了四个关系,但只需要两个。

Quick View 的默认连接属性

图 4-8. QuickView 的默认连接属性

右键单击 BANFN 和 BNFPO 的链接,选择删除连接(见图 4-9)。

Quick View 移除默认连接

图 4-9. 移除 QuickView 中的默认连接

双重检查剩余的两个关系以确保它们是正确的。表 EBAN 和 EKPO 应该通过 EBELN 和 EBELP 进行关联(见图 4-10);这些是采购订单号和采购订单项。

Quick View 确认剩余连接

图 4-10. 确认 QuickView 中剩余的连接

点击返回按钮。下一个屏幕允许选择报表字段。展开左侧的小箭头以显示表的所有字段(见图 4-11)。

Quick View 打开表格

图 4-11. QuickViewer 打开表格

在第一列中选择要显示的字段,并在第二列中选择表的选择参数(见图 4-12)。

Quick View 的选择和列表选项

图 4-12. QuickView 的选择和列表选项

接下来,重复这个过程来处理采购文档条目表。

点击执行按钮以运行报表。由于数据可能非常庞大,我们将其中一个选择标准设置为更改日期。这允许我们缩小结果数据。设置日期范围,然后点击执行按钮。在我们的示例中,我们将选择一个小的一个月数据集,只是为了看看结果是否符合预期。然后我们将为全部 10 年的数据重新运行报告。

Quick View 测试报告

图 4-13. QuickView 测试报告

报告显示所选字段(见图 4-14)。

Quick View ALV 报表

图 4-14. QuickView ALV(ABAP 列表查看器)报表

点击导出按钮(见图 4-14 中的圆圈)并选择电子表格。

Quick View 导出选项

图 4-15. QuickView 导出选项

接受 Excel 的默认设置并点击 Enter(见图 4-16)。

Quick View 导出为 xlsx

图 4-16. QuickView 导出为 xlsx

这里的格式选项取决于 SAP 版本,所以屏幕可能看起来稍有不同。无论其他可见的格式如何,确保选择 Excel。

给文件命名并保存它(见图 4-17)。

Quick View 保存对话框

图 4-17. QuickView 另存为对话框

Excel 将会自动打开。将其保存为 CSV 文件,以便轻松加载到 R 或 Python 中。

使用 R 进行导入

如果您还没有使用 R 或 R Studio 做任何事情,¹在线有许多优秀的资源,提供逐步安装指南。这与在计算机上安装任何其他软件一样简单。虽然本书不打算成为 R 的教程,但我们将介绍一些基础知识,以帮助您入门。安装了 R Studio 后,双击图 4-18 中的图标来启动它。

R Studio 图标。

图 4-18. R Studio 图标

R 中的基本概念之一是使用软件包。这些是以良好定义的格式收集函数、数据和编译代码的集合。它们使编码更加简单和一致。您需要安装必要的软件包才能使用它们。我们最喜欢的之一是tidyverse。安装此软件包有两种方法。您可以在 R Studio 的控制台窗口中使用install.packages()函数,如图 4-19 所示。只需按 Enter 键,它将为您下载并安装软件包。

从控制台窗口安装软件包。

图 4-19. 从控制台窗口安装软件包

另一种安装方法是从菜单路径 Tools → Install Packages,如图 4-20 所示。

从菜单路径安装软件包。

图 4-20. 从菜单路径安装软件包

在“Packages”行中开始键入软件包名称,然后从选项中选择,如图 4-21 所示。

从下拉选项中选择软件包。

图 4-21. 从下拉选项中选择软件包

点击“Install”按钮完成。

安装完一个软件包后,让我们开始一个新脚本。单击“New”按钮,从下拉菜单中选择 R Script,如图 4-22 所示。

开始一个新的 R Studio 脚本。

图 4-22. 开始一个新的 R Studio 脚本

现在,您将有一个空白画布,可以开始使用 R 编程语言进行数据探索。

现在,让我们开始。使用read.csv()函数轻松导入数据到 R 或 R Studio。我们使用以下设置读取文件:header设置为TRUE,因为文件有标题。我们不希望字符串设置为因子,所以stringsAsFactors设置为FALSE

小贴士

将字符串设置为因子通常是有意义的。因子表示分类数据,并可以是有序或无序的。如果您计划在加载数据后操作或格式化数据,通常不希望它们作为因子存在。您可以随时使用factor()函数将分类变量转换为因子。

最后,我们希望将任何空行或单个空格设置为 NA:

pr <- read.csv("D:/DataScience/Data/prtopo.csv",
              header=TRUE,
              stringsAsFactors = FALSE,
              na.strings=c("", " ","NA"))

数据加载后,我们可以使用head命令查看文件的一部分,如图 4-23 和 4-24 所示。

head(pr)

在 R 中查看头数据帧

图 4-23. 在 R 中查看头数据帧

在 R 中查看头数据帧继续

图 4-24. 在 R 中查看头数据帧继续

我们很快就能看到需要进行一些清理的地方。行号作为列导入,并且一些格式问题导致了一些任意的列,比如 X 和 X.1. 清理它们是我们的第一个任务。

第二阶段:清理我们的数据

在这个阶段,我们的目标是删除或纠正提取过程中明显的错误。现在花时间清理数据,可以极大地提高我们分析和建模步骤的效率。格雷格和保罗知道清理可能占据探索性数据分析时间的很大一部分,所以他们已经准备好用 R Studio 进行工作。

空值移除

首先,我们删除所有没有采购申请号的行。这是错误的数据。实际上可能没有任何需要移除的行,但这是一个良好的标准过程。确保数据的关键特征确实有条目是一个很好的开始:

pr <- pr[!(is.na(pr$Purch.Req.)), ]

二进制指示器

接下来,D 和 D.1 列是我们的采购申请的删除或拒绝指标。将其作为二进制将是一个真假指示器。我们可以很容易地通过将空白等于 0(假)和任何其他条目等于 1(真)来实现这一点。为什么使用二进制而不只是插入“已拒绝”或“未拒绝”的文本?请记住,您将会可视化和可能对这些数据进行建模。模型和可视化在处理分类变量或文本时表现不佳。然而,可视化和建模 0 和 1 是很容易的:

pr = within(`pr`, {
  deletion = ifelse(`is`.na(`D`) & is.na(`D`.1), 0, 1)
})

删除多余的列

让我们摆脱那些无用和错误的列。为什么要这样做?为什么不简单地忽略那些列?保持数据没有额外的列可以释放出用于处理的内存。在我们当前的示例中,这并不是真正必要的。然而,如果稍后我们构建一个神经网络,我们希望尽可能高效。拥有干净整洁的数据是一个简单而有效的做法²。我们创建一个列名列表,并将其赋给“drops”变量。然后我们创建一个新的数据框,它是旧数据框去掉“drops”的结果:

drops <- c("X.2","X", "Un.1", "Crcy.1", "Per.1", "X.1",
          "Purch.Req.", "Item", "PO", "Item.1", "D", "D.1",
          "Per", "Crcy")
pr <- pr[ , !(names(pr) %in% drops)]
提示

在 R 中有许多不同类型的数据结构。数据框(dataframe)是一个表格,其中每列代表一个变量,每行包含每列的值,就像 Excel 中的表格一样。

空白

处理数据时常见的问题之一是空白。空白可能会导致后续查找和合并问题。例如,您想要通过customer列合并两个数据框。一个数据框列有“Smith DrugStore”,另一个数据框有“ Smith DrugStore”。注意第二个数据框中名称前后的空格?R 将不会认为这两个客户是相同的。这些数据中的空格或空白看起来像程序的合法输入。早期删除空格和其他“隐形”元素是个好主意。我们可以使用以下代码轻松地为数据框中的所有列清理它们:

pr <- data.frame(lapply(pr, trimws), stringsAsFactors = FALSE)

lapply()函数是做什么的?阅读这些有用的函数可以让你更好地使用你的 R 代码。

Numbers

接下来,我们修改数值或整数列,使其具有这种特性。如果您的列具有数值,则不应将其存储为字符。这可能会在加载数据时发生。简而言之,值 1 不等于“1”的值。确保我们数据框中的列正确分类和正确类型是另一个关键的清理步骤,可以解决以后可能出现的问题:

pr$deletion <- as.integer(pr$deletion)
pr$Qty.Requested <- as.numeric(pr$Qty.Requested)
pr$Valn.Price <- as.numeric(pr$Valn.Price)
pr$Net.Price <- as.numeric(pr$Net.Price)

接下来,我们将在我们刚创建的数值中用零替换 NA 值。NA 表示该值不存在。R 不会假设像数量这样的离散变量在值不存在时为零。然而,在我们的情况下,我们希望 NA 值为零:

pr[,c("Qty.Requested", "Valn.Price", "Net.Price")] <-
   apply(pr[,c("Qty.Requested", "Valn.Price", "Net.Price")], 2,
        function(x){replace(x, is.na(x), 0)})

最后,我们通过用 NA 替换任何空白来清理这些分类变量。在查找缺失值时,这将很有用……空白有时看起来像分类变量中的值,因此 NA 更可靠。我们之前已经处理了空白,但这是另一个很好的实践步骤,将帮助我们避免以后的问题:

pr <- pr %>% mutate(Des.Vendor = na_if(Des.Vendor, ""),
                    Un = na_if(Un, ""),
                    Material = na_if(Material, ""),
                    PGr = na_if(PGr, ""),
                    Cat = na_if(Cat, ""),
                    Document.Type = na_if(Document.Type, ""),
                    Tax.Jur. = na_if(Tax.Jur., ""),
                    Profit.Ctr = na_if(Profit.Ctr, ""))

第三阶段:分析我们的数据

我们已经清理了数据,现在进入分析阶段。我们将回顾此阶段的两个关键目标:提出更深层次的问题来形成假设,以及为建模阶段适当地塑造和格式化数据。Greg 和 Paul 的清理过程让他们的数据处于一个继续进入分析阶段的良好位置。

DataExplorer

让我们取个捷径。这也是 R 提供的所有库的光辉的一部分。使用DataExplorer库可以进行一些非常快速和简单的数据探索。³

使用以下 R 命令安装并包括库:

install.packages("DataExplorer")
library(DataExplorer)

对数据整体结构进行快速可视化(Figure 4-25):

plot_str(pr)

使用 DataExplorer 查看数据的整体结构

图 4-25。使用 DataExplorer 查看数据的整体结构

我们可以使用DataExplorer包中的introduce命令来概述我们的数据:

introduce(pr)
      rows columns discrete_columns continuous_columns
   3361850      13                9                  4
  all_missing_columns total_missing_values complete_rows
                    0                    0       3361850
  total_observations memory_usage
             43704050    351294072

我们看到我们有超过三百万行数据,共十三列。其中九列为离散变量,四列为连续变量。

看看是否有某些列缺少大量数据非常重要。一般来说,大部分为空的列(超过 90%)在建模中没有任何价值(参见 图 4-26):

plot_missing(pr)

由于Des.Vendor字段中缺失条目较多,我们将其删除:

pr$Des.Vendor = NULL

使用 DataExplorer 识别缺失或*乎缺失的变量。

图 4-26. 使用 DataExplorer 识别缺失或*乎缺失的变量

离散特征

了解离散特征⁴有助于选择能提升模型性能的数据,以及删除没有用的数据。我们可以很容易地绘制所有离散特征的分布(图 4-27 到 4-29):

plot_bar(pr)
注意

排除具有超过 50 个条目的离散变量。

我们立即注意到一个神秘而明显的错误条目。在“文档类型”分布中有一个名为…“文档类型”的文档类型。其他离散特征也是如此。让我们找出那一行并查看一下:

pr[which(pr$Document.Type == "Document Type"),]
count(pr[which(pr$Document.Type == "Document Type"),])

我们看到了一个包含 49 个条目的列表和计数,其中文档类型为“文档类型”,而其他所有列都有列描述而不是有效值。很可能是从 SAP 提取时,在某些间隔处有头行。很容易移除:

pr <- pr[which(pr$Document.Type != "Document Type"),]

离散特征的条形图。

图 4-27. 离散特征的条形图(第一部分)

继续离散特征的条形图。

图 4-28. 离散特征的条形图(第二部分)

继续离散特征的条形图。

图 4-29. 离散特征的条形图(第三部分)

当我们再次运行plot_bar(pr)时,我们看到这些不良行已被删除。

我们还注意到一些变量没有被绘制出来。这是因为它们有超过 50 个唯一值。如果一个离散变量有太多唯一值,在模型中编码会很困难。我们可以使用以下代码来查看变量Material中唯一值的计数:

length(`unique`(`pr`$Material))

哇,我们发现我们有超过 500,000 个唯一值。让我们仔细考虑一下。材料本身能成为模型的一个良好特征吗?我们还有一个名为Matl.Group的变量,表示材料所属的分类。这可以是办公用品、IT 基础设施、原材料或类似的内容。对我们来说,这种分类比确切的材料编号更有意义。因此,我们也将删除那些材料编号的值:

pr$Material = NULL

我们还从这个条形图中注意到,变量Cat只有一个唯一值。这个变量在决定采购申请的批准或不批准方面毫无价值。我们将删除该变量:

pr$Cat = NULL

连续特征

接下来我们想要了解我们的数值/连续变量,比如 Net.Price。我们的连续变量是否呈正态钟形分布?这在建模中很有帮助,因为机器学*和神经网络更喜欢不偏斜的分布。我们怀疑连续变量都呈右偏态。一两件物品的采购申请请求会比 20 或 30 件更多。让我们看看这种怀疑是否正确。

小贴士

自然喜欢均匀/高斯分布。学校成绩,多年来或按国家划分的降雨量,以及个体身高和体重都遵循高斯分布。机器学*和神经网络更喜欢这些分布。如果您的数据不是高斯分布,对数据进行对数变换、缩放或归一化是一个不错的选择。

我们可以通过简单的直方图显示数据的分布。在 R 中使用DataExplorer包可以轻松地同时绘制所有连续变量的直方图(参见图 4-30):

plot_histogram(pr)

连续特征的直方图。

图 4-30. 连续特征的直方图

我们只关心 Qty.Requested,Valn.Price 和 Net.Price 的直方图。我们知道删除列只是我们创建的一个二进制列,其中 1 表示物品被拒绝(删除),0 表示未被拒绝。我们很快就看到所有直方图都呈右偏态,正如我们所怀疑的那样。它们向右延伸。了解这一点很重要,因为在对数据进行建模之前,我们可能需要进行一些标准化或归一化处理。

归一化将数据的尺度缩小到 0 到 1 的范围内:

X[归一化] = X−X[min / (]X[max]−X[min)]

标准化将数据的尺度缩小到均值(μ)为 0,标准差(σ)为 1:

X[标准化] = X−μ / σ

另一个测试是 QQ 图(分位数-分位数图)。这也会显示我们的连续变量是否呈正态分布。通过直方图我们知道这些分布不是正态分布。这里的 QQ 图仅供示意。

如果 QQ 图呈对角直线,说明数据是正态分布的。通过我们的观察,我们很快就能看出这些变量并不呈正态分布。DataExplorer 中的 QQ 图(参见图 4-31 以查看有趣的连续特征,和图 4-32 以查看删除标记)默认将数据与正态分布进行比较:

plot_qq(pr, sample=1000L)

连续特征的 QQ 图。

图 4-31. 连续特征的 QQ 图

QQ 图显示数据不符合正态分布

图 4-32. QQ 图显示数据不符合正态分布

第四阶段:建模我们的数据

现在我们已经熟悉了数据,是时候将其塑造并输入神经网络以检查它是否能学会购买申请是否被批准或拒绝。我们将在 R 中使用 TensorFlow 和 Keras 来完成这个任务。Greg 和 Paul 知道建模阶段才是实际价值被提取的地方——如果他们正确地进行建模,他们知道将通过“收集、清洗和分析”阶段获得宝贵的见解。

TensorFlow 和 Keras

在我们深入研究我们的模型之前,我们应该稍作停顿,讨论一下 TensorFlow 和 Keras。在数据科学和机器学*领域,它们是两个最广泛使用的工具。

TensorFlow 是一个开源软件库,特别是自 2017 年发布 1.0.0 版本以来,在数值计算方面迅速被广泛使用。尽管高性能数值计算适用于许多领域,但 TensorFlow 在 Google Brain 团队的 AI 专注中成长起来。这种血统使其设计对机器学*和深度学*任务具有高适应性。

尽管 TensorFlow 最辛勤工作的代码是高度调整和编译的 C++,它为简单消费提供了出色的 Python 和 R API。你可以直接使用TensorFlow编程,或者使用 Keras。Keras 是 TensorFlow 的高级 API,用户友好、模块化且易于扩展。你可以在 Windows、macOS、Linux 和 Android/iOS 上使用 TensorFlow 和 Keras。TensorFlow 宇宙中最酷的部分是,Google 甚至创建了定制硬件来增强 TensorFlow 的性能。Tensor Processing Units (TPUs)是 AlphaGo 和 AlphaZero 最先进版本的核心,这些专注于游戏的 AI 征服了围棋——长期以来被认为是机器精通的几十年之外的游戏。

核心 TensorFlow 非常适合在复杂的数据科学场景中设置强大的计算。但对于数据科学家来说,通常有助于在更高的抽象级别建模工作,并抽象掉一些底层细节。

进入 Keras。它足够可扩展,可以在几个主要的底层 ML 工具包(如 TensorFlow、Theano 或 Microsoft Cognitive Toolkit)之上运行。Keras 的设计侧重于 Python 和 R 的用户友好性,快速建立和实验深度神经网络模型。作为数据科学家,我们知道快速实验提供了最好的结果——它们允许你快速失败并朝着更正确的方向前进!

快速停顿结束。让我们回到场景中。我们将稍后在 TensorFlow 和 Keras 中使用它们,但首先我们会使用基本的 R 编程。

训练和测试分离

这个过程的第一步是将数据分割成训练集和测试集。使用rsample很容易实现这一点。

tt_split <- initial_split(pr, prop=0.85)
trn <- training(tt_split)
tst <- testing(tt_split)

在 R Studio 的全局环境中查看,有两个新的数据框:TRN 用于训练,TST 用于测试(图 4-33)。

训练和测试数据框的视图。

图 4-33. 训练和测试数据框的视图

数据整形和独热编码

我们仍在为 TensorFlow 和 Keras 整形我们的数据过程中。我们将在接下来的步骤中继续基本的 R 编程。下一步是整形数据,使其可以与神经网络很好地配合。总的来说,神经网络对正态分布的数据表现最佳。我们正在向网络中输入的数据必须是名义的:我们不能将我们在采购申请数据中发现的分类变量输入到模型中。网络不会知道如何处理诸如“物料组”之类的内容。我们将使用称为 独热编码 的过程将我们的分类数据转换为稀疏数据。⁵ 例如,Matl.Group 列的独热编码的结果将如 图 4-34 所示。

独热编码的可视化。

图 4-34. 独热编码的可视化

我们知道我们想要对分类变量进行独热编码,但对于其他变量,我们想要做些什么呢?考虑到请求数量列和采购申请的数量选项。新车的采购申请可能不会超过一个。然而,原材料批次的请求数量可能是一千磅。这让我们很好奇,请求数量列的值范围是多少?我们可以轻松地用以下命令看到:

max(pr$Qty.Requested)
min(pr$Qty.Requested)

我们看到值的范围是从 0 到 986。什么?零的数量?有多少个?

count(`pr`[which(`pr1`$Qty.Requested == 0),])

我们看到有 313 行的数量为 0!这是什么意思?我们对这些数据感到困惑,那我们应该将它们删除吗?数据科学并不是真空的,尽管我们编码人员希望它是。我们必须回到业务部门,拿几个数量为零的采购申请的例子,并问他们是否知道原因。如果他们不知道,那么我们将删除这些数量为零的行。

通过这个过程,我们学到了一些东西。当问及帕特关于这些奇怪的申请时,他说:“有时候当我不在电脑旁,有人打电话来询问我拒绝的采购申请,他们会将数量归零,因为他们没有拒绝该行的权利。” 实质上,数量为零的采购申请是被拒绝的采购申请。我们必须将这些的删除指示器转换为 1,以表示它们被拒绝:

pr = within(`pr`, {
    deletion = ifelse(`Qty`.Requested == 0, 1 ,0)
})

现在我们已经适当处理了数量为零的采购申请,我们回到手头的任务。模型将无法在从 0 到一千的单个变量上表现最佳。将这些订单数量分成几组将使模型表现更好。我们将创建三个值桶。我们选择这个值相当随意,可以在测试模型性能时稍后更改。

配方

我们决定对我们的分类变量进行独热编码,以及对我们的数值变量进行缩放和分桶。为此,我们将在 R 中使用recipes库。这个非常方便的库允许我们为数据转换创建“配方”。

recipes的概念很直观:定义一个配方,以便稍后应用编码和处理。最终的结果可以应用于机器学*或神经网络。

我们已经决定了如何处理数据以准备用于网络。让我们通过recipes包中的代码来实现这一点。

首先,我们想创建一个recipe对象,定义我们要分析的内容。在这段代码中,我们说我们希望根据数据中的其他特征预测删除指示器:

library(recipes)
recipe_object <- recipe(deletion ~ Document.Type +
                    PGr +
                    Matl.Group +
                    Qty.Requested +
                    Un +
                    Valn.Price +
                    Tax.Jur. +
                    Profit.Ctr,
                    data = trn)
#We could also just use the . like this to indicate all, but the above is done 
#for clarity. recipe_object <- recipe(deletion ~ ., data = trn)

注意

如果遇到内存错误,如“Error: cannot allocate vector of size x.x Gb”,可以使用以下命令增加内存分配(前两个数字表示分配的几个 Gigabytes,本例中为 12):

memory.limit(1210241024*1024)

我们的下一步是取出recipe对象,并对其应用一些成分。我们已经说明了我们希望将我们的数量和价格值放入三个桶中。我们使用recipes中的step_discretize函数来实现这一点:

提示

一些建模者更喜欢分箱,而另一些则喜欢保持连续变量的连续性。在这里,我们分箱以提高模型的性能。

recipe_object <- recipe_object %>%
   step_discretize(Qty.Requested, options = list(cuts = 3)) %>%
   step_discretize(Valn.Price, options = list(cuts = 3))

我们还希望对所有的分类变量进行独热编码。我们可以一次列出它们,也可以使用recipes包中的许多选择器之一。我们使用step_dummy函数执行编码,并使用all_nominal选择器选择所有的分类变量:

recipe_object <- recipe_object %>%
  step_dummy(all_nominal())

然后我们需要对所有的值进行缩放和居中处理。如前所述,我们的数据不服从高斯分布,因此需要进行某种形式的缩放:

rec_obj <- rec_obj %>%
  step_center(all_predictors()) %>%
  step_scale(all_predictors())

提示

有许多归一化方法;在我们的例子中,我们使用了最小-最大特征缩放和标准分数。

到目前为止,我们还没有对这个配方做任何操作。现在我们需要准备数据,并使用prep命令将配方应用到数据上:

recipe_trained <`-` prep( recipe_object, training = trn, retain = TRUE)

现在我们可以将这个配方应用到任何我们有的数据集上。我们将从我们的训练集开始,并在命令中排除删除指示器:

x <- bake(rec_obj, new_data = trn) %>% select(-deletion)

为神经网络准备数据

现在我们完成了我们的配方,需要为神经网络准备数据。

提示

我们最喜欢(并且通常被接受为最好的)的技术是直接跳入神经网络模型。最好是从最简单的模型逐步增长到最复杂的模型,设定一个性能标准,然后尝试用越来越复杂的模型超越它。例如,我们应该首先尝试简单的线性回归。因为我们试图分类批准和未批准的采购申请,我们之后可以尝试分类机器学*技术,如支持向量机(SVM)和/或随机森林。最后,我们可能会使用神经网络。然而,出于教学目的,我们将直接使用神经网络。没有先验知识导致了这个决定;这只是一个教学示例。

首先,我们要创建一个删除值的向量:

training_vector <- pull(`trn`, deletion)

如果这是你第一次使用 TensorFlow 和 Keras,你需要安装它。这与常规库有些不同,所以我们在这里介绍一下步骤。首先,像安装任何其他包一样安装包:

install.packages("tensorflow")

然后,要使用 TensorFlow,你需要在库声明之后进行额外的函数调用:

library(tensorflow)
install_tensorflow()

最后,很好的流程是使用以下常见的打印语句检查它是否工作。如果你得到了“Hello, TensorFlow!” 的语句,那么它正在工作:

sess = tf$Session()
 hello <- tf$constant('Hello, TensorFlow!')
 sess$run(hello)

Keras 安装与其他 R 库类似。让我们在 Keras 中创建我们的模型。第一步是初始化模型,我们将使用 keras_model_sequential() 函数进行初始化:

k_model <- keras_model_sequential()

模型由多层组成。接下来的步骤是创建这些层。

我们的第一层是一个输入层。输入层需要输入的形状。随后的层根据第一个输入层推断形状。在我们的情况下,这很简单,输入形状是我们训练集中列的数量 ncol(x_trn)。我们将单元数设置为 18。在测试神经网络时有两个关键决策要考虑。这些是每层单元的数量和层数。

我们的下一层是一个隐藏层,具有相同数量的输入。请注意,它与前一层相同,但我们无需指定形状。

我们的第三层是一个 10% 的 Dropout 层。也就是说,该层的神经元中将随机地失活 10%。

提示

Dropout 层控制过拟合,当模型在某种程度上记住了训练数据时发生。当这种情况发生时,模型在未见过的数据上表现不佳……这有点违背了神经网络的目的。Dropout 在训练阶段使用,随机地使一组神经元失活。

我们的最后一层是输出层。单元数为 1,因为结果是互斥的。也就是说,要么采购申请被批准,要么没有。

最后,我们将编译模型或 构建 它。我们需要设置三个基本的编译设置:

优化器

调整模型权重的技术。一个非常常见的起点是Adam优化器。

初始化器

模型设置层的初始随机权重的方式。有许多选项;一个常见的起点是uniform

激活

参考第二章了解激活函数的描述。Keras 提供了许多易于使用的激活函数

k_model %>%
  #First hidden layer with 18 units, a uniform kernel initializer,
  #the relu activation function, and a shape equal to 
  #our "baked" recipe object. 
  layer_dense(
    units = 18,
    kernel_initializer = "uniform",
    activation = "relu",
    input_shape = ncol(x_trn)) %>%

  #Second hidden layer - same number of layers with
  #same kernel initializer and activation function.
  layer_dense(
    units = 18,
    kernel_initializer = "uniform",
    activation = "relu") %>%

  #Dropout
 layer_dropout(rate = 0.1) %>%

  #Output layer - final layer with one unit and the same initializer
  #and activation. Good to try sigmoid as an activation here. 
  layer_dense(
    units = 1,
    kernel_initializer = "uniform",
    activation = "relu") %>%

  #Compile - build the model with the adam optimizer. Perhaps the 
  #most common starting place for the optimizer. Also use the 
  #loss function of binary crossentropy...again, perhaps the most 
  #common starting place. Finally, use accuracy as the metric 
  #for seeing how the model performs. 
  compile(
    optimizer = "adam",
    loss = "binary_crossentropy",
    metrics = c("accuracy"))
提示

设置神经网络的参数既是艺术也是科学。调整层中神经元的数量、丢失率、损失优化器等。这是您实验和调整网络以获得更高准确度和更低损失的地方。

要查看模型,请键入**k_model**

___________________________________________________________________________
 Layer (type)                       Output Shape                    Param #     
 =========================================================================
 dense_2 (Dense)                    (None, 18)                      2646        
 ___________________________________________________________________________
 dropout_1 (Dropout)                (None, 18)                      0           
 ___________________________________________________________________________
 dense_3 (Dense)                    (None, 18)                      342         
 ___________________________________________________________________________
 edropout_2 (Dropout)               (None, 18)                      0           
 ___________________________________________________________________________
 dense_4 (Dense)                    (None, 1)                       19          
 =========================================================================
 Total params: 3,007
 Trainable params: 3,007
 Non-trainable params: 0
 ___________________________________________________________________________

最后一步是将模型拟合到数据上。我们使用了用配方“烘焙”的数据,即x_trn

history <- fit(
    #fit to the model defined above
  object = k_model,
      #baked recipe
  x = as.matrix(x_trn),
    #include the training_vector of deletion indicators
  y = training_vector,
    #start with a batch size of 100 and vary it to see performance
  batch_size = 100,
    #how many times to run through?
  epochs = 5,
    #no class weights at this time, but something to try
    #class_weight <- list("0" = 1, "1" = 2)
    #class_weight = class_weight,
  validation_split = 0.25)

模型在运行时显示日志:

Train on 1450709 samples, validate on 483570 samples
Epoch 1/5
1450709/1450709 [==============================] 
- 19s 13us/step - loss: 8.4881e-04 - acc: 0.9999 -
val_loss: 0.0053 - val_acc: 0.9997
Epoch 2/5
1450709/1450709 [==============================] 
- 20s 14us/step - loss: 8.3528e-04 - acc: 0.9999 -
val_loss: 0.0062 - val_acc: 0.9997
Epoch 3/5
1450709/1450709 [==============================] 
- 19s 13us/step - loss: 8.5323e-04 - acc: 0.9999 -
val_loss: 0.0055 - val_acc: 0.9997
Epoch 4/5
1450709/1450709 [==============================] 
- 19s 13us/step - loss: 8.3805e-04 - acc: 0.9999 -
val_loss: 0.0054 - val_acc: 0.9997
Epoch 5/5
1450709/1450709 [==============================] 
- 19s 13us/step - loss: 8.2265e-04 - acc: 0.9999 -
val_loss: 0.0058 - val_acc: 0.9997

结果

我们希望从我们的模型中得到的是高准确率,并随着时代的推移得到改进。然而,这并不是我们看到的。请注意图中的第二张图表图 4-35。我们看到准确率从一开始就非常高,并且从未改善。损失函数也没有减少,而是保持相对稳定。

这告诉我们模型没有学到任何东西。或者更确切地说,它很快学到了一些东西,使其非常准确,并从那时起停止学*。我们可以尝试许多调整选项,也许不同的优化器和损失函数。我们还可以重新设计神经网络,增加或减少层数。但是,让我们高层次地思考一分钟,回到原始数据,并提出一些问题。

我们是否从一开始就选择了正确的 SAP 特征?还有其他可能有帮助的特征吗?

模型学*的准确性和损失结果

图 4-35. 模型学*的准确性和损失结果

我们在途中是否犯过错误或者做出了错误的假设?这需要对整个过程进行回顾。

这些数据可以被建模吗?并非所有数据都适合建模。

在回答这些问题之后,我们偶然发现了这一点。如果批准的采购申请数量过多怎么办?如果模型只学会了一直回答“是”,因为在训练过程中几乎总是正确的答案呢?如果我们回过头来看一下建模之前的数字,我们会发现帕特批准了超过 99%的所有采购申请。我们可以尝试不同的模型和数据中的不同特征,但是数据探索传奇的真相很可能是这些数据不能被建模。或者更确切地说,它可以被建模,但是由于批准数量过多,模型只会学会批准。它将发现自己有很高的准确性和低损失,因此表面上看是一个很好的模型。

总结

尽管我们未能对采购申请数据进行建模,但这个例子教会了我们许多宝贵的经验。有时候数据无法被建模,它就是这样发生的……而且经常发生。一个准确率高、损失低的模型并不意味着它是一个好模型。我们的模型准确率达到了 99%,这从一开始就应该引起怀疑。但它是一个毫无价值的模型;它没有学到任何东西。数据科学家通常的角色是报告发现并提出下一步的建议。我们失败了,但我们快速失败并能够朝着正确的解决方案迈进。

可以说,格雷格和保罗辜负了帕特。毕竟,我们无法根据我们找到和探索的数据做出任何良好的预测。但仅仅因为我们没有找到预测建模方案的方法,并不意味着我们失败了。我们学到了!如果数据科学真的是科学,它必须承认负面结果和正面结果一样。我们没有学会预测采购申请行为,但我们确实了解到尝试这样做不具成本效益。我们了解到帕特和他的同事们创建了一套使企业在采购行为上非常有纪律性的稳健流程。

在探索性数据分析中,唯一的失败是没有学到东西。模型可能没有学到东西,但数据科学家们学到了。格雷格和保罗为此多做了一次去咖啡机的旅行来庆祝。

在本章中,我们确定了一个业务需求,从 SAP 中提取了必要的数据,清理了数据,探索了数据,建模了数据,并从结果中得出结论。我们发现我们无法让我们的模型学*当前的数据,并推测这是因为数据明显偏向批准。在这一点上,我们正在做出合理的猜测;我们可以做得更多。

我们还可以采取其他方法。例如,我们可以使用编码器来增强数据,这超出了本书的范围。我们可以对变量进行加权,使被拒绝的采购申请比被接受的具有更大的价值。然而,在测试这种方法时,模型简单地失去了所有的准确性,因为完全不同的原因失败了。我们还可以将被拒绝的采购申请视为异常,并采用完全不同的方法。在第五章中,我们将深入研究异常检测,这可能为应用于这些数据提供其他答案。

我们决定在我们的示例中采取的最终行动方案并不是一个数据方法(这让我们感到遗憾)。应该告知业务,因为超过 99%的所有采购申请都得到批准,模型无法找到显著特征来确定何时会发生拒绝。如果不做大量的工作,这可能是个死胡同。也许有不同的 IT 解决方案,比如一个手机应用程序可以帮助帕特更有效地完成工作。然而,这种解决方案很可能无法通过机器学*和数据科学找到。

¹ 安装 R Studio 和 R 的说明,请访问https://www.rstudio.com/products/rstudio/download/

² 我们以前提到过这个链接,但我们会再次链接(因为它非常好):https://vita.had.co.nz/papers/tidy-data.pdf

³ 深入了解 DataExplorer,可参考https://cran.r-project.org/web/packages/DataExplorer/vignettes/dataexplorer-intro.html提供的详细介绍。

⁴ 请记住,在第二章中提到,离散或分类特征是具有明确定义边界的特征。例如颜色或狗的种类。

⁵ 有时也称为创建“虚拟变量”。

第五章:使用 R 和 Python 进行异常检测

麦克森公司(McKesson),美国最大的药品分销商之一,同意支付创纪录的 1.5 亿美元民事罚款,涉嫌违反《受控物质法》(CSA),司法部今天宣布。

司法部,2017 年 1 月 17 日

阅读这些头条新闻时,简妮的心沉了下去。她专心阅读了文章;这影响了她。她在大宝矿库的监管部门工作,负责维护公司的合规性。她知道《可疑订单监控法规》(21 C.F.R. 1301.74(b))。最*,司法部因不符合该法规而连续打击公司,比过去更为频繁。法规大致规定,从事受控物质的制造和分销公司“了解他们的客户”。法规的确切措辞是,

对于健全运营来说,处理人员采取合理措施识别他们的客户、了解这些客户通常进行的正常和预期交易,从而识别客户进行的那些可疑交易是至关重要的。

21 C.F.R. 1301.74(b)

但这究竟意味着什么?她知道她的公司在 SAP 中有销售订单。已经有超过 10 年的销售订单了。但了解客户的正常和预期交易意味着什么?她决定把这个问题带给 SAP 团队,并与他们讨论,如果可能的话,可以采取什么措施来保护他们免受违规行为和潜在罚款的影响。

杜安,SAP 团队的分析师,对她的问题很感兴趣。SAP 包含/存储销售订单和销售订单历史记录,但它没有提供订购模式或检测系统,以便在出现异常时进行检测。首先要问的问题是:“什么是异常?”这并不像简单地说一个客户通常一周订购 5 件产品,然后突然订购 10 件那么简单。如果他们错过了一周怎么办?如果他们一直在稳步增加他们的供应链,现在 10 件就是可以接受的?

当杜安把问题带给数据科学家格雷格和保罗时,他们立刻嗅到了异常检测问题。异常检测基本上是一种识别数据中不符合预期模式的异常模式的方法。我们大多数人已经通过数据科学体验过异常检测。当你收到信用卡公司的短信或电话询问最*的交易时,你就体验过异常检测。欺诈检测是信用卡公司用来防止损失的一种复杂的异常检测方法。

Greg 和 Paul 启动了他们的程序编辑器,准备找出那些异常。Duane 逗留在旁边,主要是为了向 Greg 和 Paul 提供 SAP 的见解,但也想看看他们是如何做到这一点的。Duane 相当确定,虽然他没有统计学或计算机科学的博士学位,但他能够跟上足够的速度开始理解。

异常类型

有三种一般类型的异常:

数据中的异常由一个显著的离群值确定。在我们的订购模式示例中,假设一个客户通常每周订购 10 个项目,但某周他们订购了 100 个。这种订单量比他们典型订单增加了 10 倍,是一个简单的点异常。

环境

在一个条件内的异常。通常,这是对时间序列数据的分析。以销售数据为例,假设一个客户整年都订购很多产品,但七月份订购手套。十二月份订购手套不是异常,但七月份是。

集体

在数据集的上下文中查看的异常。这与数据整体的模式相关,如心电图或正弦波。如果客户的订购模式突破了他们典型的波动或模式,这将是异常的。

或许检测异常的最简单方法是,如果数据点距离均值或中位数有一定标准偏差,则将其标记为异常。销售数据是一个时间序列,所以必须考虑一个滚动窗口。定义滚动窗口的宽度将由业务条件决定,因为每个企业的情况都有所不同。在我们怀疑的订单检测案例中,三年的窗口是合适的。滚动窗口(或滚动平均)平滑短期波动并强调长期波动。在我们当前的情况下,我们可以使用回归并带有容差地拟合线条。任何超出容差范围的东西都是异常。

然而,这是一个相当静态的方法,对我们来说……不够。它不会考虑到任何背景信息,比如季节性。我们不知道我们的销售订单数据是否有季节性;因此,为了慎重起见,我们至少应该检查一下。我们知道我们的要求是了解我们的客户,所以我们希望看到随时间变化的订购模式。简而言之,我们想识别集体异常。

R 中的工具

有许多文档详细描述的技术,用于使用静态方法(如前面讨论过的方法)、机器学*技术和神经网络模型来检测异常。在考虑使用哪种技术时,请回答以下问题:

  • 它是什么类型的数据?

    对于我们的情景来说,这是时间序列数据。

  • 数据被标记了吗?

    我们没有标记的数据,因此我们处于无监督学*的场景中。¹ 如果数据被标记,它可以被分成测试和训练集,并基本上被转化为分类问题。

数据科学家工具箱中有许多工具可用于使用时间序列数据进行无监督学*。我们可以采用 TensorFlow/Keras 构建递归神经网络。这将大大复杂化事情,并且不一定(实际上不太可能)会比为此目的构建的几个 R 库和 Python 包提供更好的结果。

提示

在 R 中有小品文档(vignettes)。这些是包的教程,大多数时候非常信息丰富和有用。要查看您包中可用的小品文档,请在 R 工具的控制台中输入 **browseVignettes()**。例如,要查看关于 dplyr 的详细教程,请输入 **browseVignettes(“dplyr”)**。在这种情况下,这个关键包在 R 中有多个教程。

AnomalyDetection

这是一个由 Twitter 开源的包。它专门用于检测时间序列数据中的异常……正是我们正在寻找的。它基于季节性极端学生化偏差(ESD),而季节性极端学生化偏差则基于广义 ESD。广义 ESD 是一种检测*似正态分布的单变量数据异常的测试方法。广义 ESD 的优点在于,它不需要指定异常值的数量,只需指定疑似异常值的上限。

给定这个上限,假设为 u,该包实际上执行 u 个测试,首先是一个异常测试,然后是两个,依此类推,直到 u 个异常。

Anomalize

anomalize package 是对像 R 和 Python 这样的编程语言功能和能力的又一个证明。在 CRAN 上有关于这个包的 精彩文档

这个包被设计用于检测所有类型的异常:点异常、上下文异常和集体异常。它是由 Business Science. 开发的 Twitter AnomalyDetection 包的可扩展适应版。这是一个基于时间序列的异常检测包,可以适用于一个到多个时间序列。

我们了解来自 SAP 所需的情景和数据。我们还在 R 中为检测异常识别了一些有用的库。我们的任务由司法部定义。我们需要理解我们的客户及其订购模式。换句话说,我们想知道何时发生异常。接下来的步骤将详细介绍将我们的 SAP 订单数据转化为异常报告的过程。

获取数据

有许多方法可以从 SAP 中获取数据。对于我们的异常检测场景,我们可以简单地通过 CSV 下载数据,通过 SAP Gateway 读取,或使用 CDS 视图。我们可能需要或不需要将数据存储在单独的系统中,但为了说明目的,我们将在这里执行这些操作。在我们的场景中,我们将通过 NetWeaver Gateway 从 SAP 中读取数据,使用 SQL Server Integration Services(SSIS)将数据从 SQL 中提取到 Power BI 中。最后,我们将使用 R 和 Python 代码创建一个交互式仪表板进行异常检测。

SAP ECC 系统

我们的第一步是定义我们想要分析的数据结构。我们希望看到按材料和客户的销售订单行数量。

在 SAP 中输入事务码**SE11**,为数据结构命名,然后点击创建按钮(图 5-1)。

SAP 数据字典

图 5-1. SAP 数据字典

选择“Structure”单选按钮,然后点击 Enter(图 5-2)。

SAP 结构选择

图 5-2. SAP 结构选择

我们想要收集销售订单、销售订单项目、订购材料、数量、客户和订单日期。输入数据和字段的简短描述(图 5-3)。(保持原始 SAP 字段名称作为字段名称使得与网关的集成更简单。我们将在网关之外将它们重命名为有意义的名称。)

数据结构的组成部分

图 5-3. 数据结构的组成部分

点击货币/数量字段选项卡,为数量字段添加参考值。

如果你不知道参考表和字段是什么,你可以参考我们将要读取的表格。在这种情况下,这些表格是 VBAK(销售订单头)和 VBAP(销售订单项目)。

从 VBAP 表复制的货币和数量字段用于数量(KWMENG)。

图 5-4. 从 VBAP 表复制的货币和数量字段用于数量(KWMENG)

按照菜单路径 Extras → Enhancement Category 输入该结构是否可以增强。我们不需要增强,所以我们将选择不能增强(图 5-5 和 5-6)。

数据结构增强类别

图 5-5. 数据结构增强类别

增强类别的选择

图 5-6. 增强类别的选择

点击“复制”按钮,然后点击“激活”按钮。弹出对话框要求分配一个包以分配此结构。出于我们的目的,我们使用的是一个$TMP 对象,因此不会进行传输。

激活 SAP 结构

图 5-7. 激活 SAP 结构

如果所有操作都正确完成,屏幕底部将显示“对象已保存并已激活”的确认消息。

接下来,点击“返回”按钮。函数模块需要一个表类型。我们现在将创建它。

在结构的名称前面加上“_TT”,与结构名称相同,如 图 5-8 中所示。

SAP 创建表类型

图 5-8. 在 SAP 中创建表类型

点击“创建”按钮。这次选择“表类型”单选按钮,并按 Enter 键(参见 图 5-9)。

SAP 表类型选择

图 5-9. SAP 表类型选择

输入“简短文本”,并在“行类型”字段中放入先前创建的结构(参见 图 5-10)。

点击“激活”按钮。系统会再次询问您选择一个包,并在完成后显示“对象已保存并激活”的确认信息。

将结构分配给表类型

图 5-10. 将结构分配给表类型

下一步是为数据读取创建一个函数模块。在命令行输入事务码 **SE37** 并按 Enter 键。为函数模块命名,并点击“创建”按钮(参见 #sap_function_builder)。输入“函数组”和“简短文本”描述,然后点击“保存”按钮。如果您不知道函数组,可以向您的 Basis 管理员或 ABAP 开发人员获取。

SAP 功能生成器

图 5-11. SAP 功能生成器

您将收到一条信息提示(#sap_function_module_warning)。点击 Enter 键继续。

SAP 函数模块警告

图 5-12. SAP 函数模块警告

点击“属性”选项卡,确保函数模块是远程可用的(参见 图 5-13)。如果没有这个选项,NetWeaver Gateway 将无法调用此函数。

SAP 远程启用函数选择

图 5-13. SAP 远程启用函数选择

点击“导出”选项卡,输入参数名称和关联类型,即先前创建的表类型。选中“传递数值”复选框,确保数据传递到此导出参数(参见 图 5-14)。

函数模块的参数

图 5-14. 函数模块的参数

点击“源代码”选项卡。输入以下代码。它将读取所有大于 2014 年 01 月 01 日的销售订单行项目。一般来说,添加一个导入日期参数会更加动态化。但是,为了说明的目的,我们会保持这个超级简单:

`SELECT` `DISTINCT` vbak~vbeln
                vbak~erdat
                vbak~kunnr
                vbap~posnr
                vbap~matnr
                vbap~kwmeng
  `INTO` `CORRESPONDING` `FIELDS` `OF` `TABLE` e_salesdata
  `FROM` vbak `JOIN` vbap `ON` vbak~vbeln = vbap~vbeln
  `WHERE` vbak~erdat >= '2014101'.

点击激活按钮,然后点击测试按钮确保它正常运行且不运行时间过长。点击执行按钮。函数将返回一张数据表。如果表很大,例如我们所拥有的这张表,不要点击查看表按钮。SAP 将尝试渲染整个表格并遇到分页问题,然后会简单退出。

功能模块测试结果

图 5-15. 功能模块测试结果

SAP NetWeaver 网关

现在是关于 SAP NetWeaver 网关的说明。正如前面指出的,该实用程序的主要功能是通过 OData feed(XML 或 JSON)公开我们新发现的数据。如果您的 SAP 环境有单独的 NetWeaver 网关服务器,请登录并输入事务码**SEGW**。点击创建按钮以创建新项目。输入项目名称、描述和包名称。然后点击输入按钮(图 5-16)。

在 SAP 中创建新的网关项目

图 5-16. 在 SAP 中创建新的 NetWeaver 网关项目

您的新项目将显示在下方(图 5-17)。

在事务 SEGW 中的项目

图 5-17. 事务 SEGW 中的项目

右键单击数据模型节点,并从上下文菜单中选择导入 → RFC/BOR 接口来读取我们刚刚创建的函数定义(图 5-18)。

从功能模块导入 RFC 到模型中

图 5-18. 将 RFC 导入模型

输入实体类型名称,选择目标系统,并输入函数模块的名称。在此示例中,由于我们的网关嵌入在 SAP ERP 中,我们使用本地选项。然后点击下一步按钮(图 5-19)。

定义网关模型的 RFC

图 5-19. 定义网关模型的 RFC

选择所有用于服务的元素的复选框。我们需要所有这些,因此只需选择顶级节点。点击下一步按钮(图 5-20)。

从 FM 导出中选择必要字段

图 5-20. 从功能模块导出中选择必要字段

确定结构的关键字段,然后点击完成按钮(图 5-21)。关键字段是结构的唯一字段,在这种情况下是销售订单(VBELN)和销售订单项目(POSNR)。在 SAP 系统中,同一销售订单项目永远不会有相同的销售订单号。

确定模型的关键字段

图 5-21. 确定模型的关键字段

打开项目文件夹 数据模型 → 实体类型 → 销售订单,并单击属性文件夹。在名称列中为服务添加一些有意义的名称(如 图 5-22 所示),并通过点击复选框确定可以为空的任何值。完成后,点击生成运行时对象按钮。

为实体命名

图 5-22. 给实体赋予有意义的名称

接受即将生成的对象的默认值。点击 Enter 按钮 (图 5-23)。

生成模型和服务定义

图 5-23. 生成模型和服务定义

在弹出对话框中,如有必要,再次分配到传输。点击保存按钮 (图 5-24)。

为网关项目选择一个包

图 5-24. 选择网关项目包

打开项目文件夹 服务实现 → SalesorderSet,并右键单击 GetEntitySet(查询)行 (图 5-25)。选择映射到数据源选项,如图 5-26 (图 5-26) 所示。

映射网关到数据源

图 5-25. 将网关映射到数据源

选择映射到源选项

图 5-26. 选择映射到数据源选项

这将为我们的服务和后端函数提供映射选项。确定函数模块的目标系统、类型和名称。按下 Enter 键 (图 5-27)。

标识源函数模块

图 5-27. 标识源函数模块

将每个函数模块字段映射到服务字段。如果您使用常见的参数类型定义函数,您可以更轻松地点击建议映射 (图 5-28)。在这个示例中,效果很好。

使用建议映射选项进行映射

图 5-28. 使用建议映射选项进行映射

完成后,点击生成运行时对象按钮 (图 5-29)。

打开服务维护文件夹,右键单击要使用的中心,并选择注册 (图 5-30)。

服务维护的节点

图 5-29. 服务维护的节点

选择注册网关组件

图 5-30. 选择注册网关组件

选择系统别名,然后点击 Enter 按钮 (图 5-31)。

为网关服务标识系统别名

图 5-31. 为网关服务标识系统别名

系统现在将注册服务。接受默认条目,分配一个包,并点击“输入”按钮(图 5-32)。

生成和激活网关服务和组件

图 5-32. 生成和激活网关服务和组件
注意

如果在服务维护文件夹下没有中心,则系统配置不正确。请参考您的基础管理部门以获取帮助。在blogs.sap.com有一些非常好的博客可供参考。

让我们测试一下,看看是否有效。再次右键单击中心,但这次选择维护(图 5-33)。

维护网关服务

图 5-33. 维护网关服务

显示了带有一些额外功能的服务。最简单的方式是在浏览器中调用服务,所以点击“调用浏览器”按钮(图 5-34)。

在浏览器中测试网关服务

图 5-34. 在浏览器中测试网关服务

默认浏览器打开服务和实体集:

<app:service xml:base="http://<host>:<port>/sap/opu/odata/sap/ZGMF_SALES_SRV/">
  <app:workspace>
  <atom:title type="text">Data</atom:title>
  <app:collection sap:creatable="false" sap:updatable="false"
     sap:deletable="false" sap:pageable="false"
     sap:content-version="1" href="SalesOrderSet">
     <atom:title type="text">SalesOrderSet</atom:title>
     <sap:member-title>SalesOrder</sap:member-title>
  </app:collection>
</app:workspace>
<atom:link rel="self" 
  href="http://<host>:<port>/sap/opu/odata/sap/ZGMF_SALES_SRV/"/>
<atom:link rel="latest-version" 
  href="http://<host>:<port>/sap/opu/odata/sap/ZGMF_SALES_SRV/"/>
</app:service>

要尝试实际的实体集和功能模块,请复制实体集名称(在我们的情况下为SalesOrderSet),并将其放在 URL 的/?之间。对于我们的示例,此 URL 将起作用:

https://[YOUR_SAP_HOSTNAME]/sap/opu/odata/sap/ZGMF*`_SALES_`*SRV/SalesOrderSet?
    $format=xml

并将生成如下输出:

...
<`id`>
  http://<`host`>:<`port`>/sap/opu/odata/sap/YGMF_SALES_SRV/SalesOrderSet
</`id`>
<`title` type="text">SalesOrderSet</`title`>
<`updated`>2019-04-25T17:39:37Z</`updated`>
<`author`>
  <`name`/>
</`author`>
<`link` href="SalesOrderSet" rel="self" title="SalesOrderSet"/>
<`entry`>
  <`id`>
http://<`host`>:<`port`>/sap/opu/odata/sap/YGMF_SALES_SRV/SalesOrderSet (Vbeln='5000000',Posnr='000010')
  </`id`>
  <`title` type="text">SalesOrderSet(Vbeln='5000000',Posnr='000010')</`title`>
  <`updated`>2019-04-25T17:39:37Z</`updated`>
  <`category` term="YGMF_SALES_SRV.SalesOrder"
scheme="http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"/>
  <`link` href="SalesOrderSet(Vbeln='5000000',Posnr='000010')"
     rel="self" title="SalesOrder"/>
  <`content` type="application/xml">
    <`m``:``properties`>
      <`d``:``Vbeln`>5000000</`d``:``Vbeln`>
      <`d``:``Erdat`>2017-07-03T00:00:00</`d``:``Erdat`>
      <`d``:``Posnr`>000010</`d``:``Posnr`>
      <`d``:``Matnr`>12345678</`d``:``Matnr`>
      <`d``:``Kwmeng`>1.000</`d``:``Kwmeng`>
      <`d``:``Kunnr`>56789</`d``:``Kunnr`>
    </`m``:``properties`>
  </`content`>
 </`entry`>
...

现在我们知道我们的数据源正在工作,我们可以继续使用 SSIS 进行读取并将其放入 SQL 中。对于高容量和频繁访问的分析数据,将其存储在中间 SQL 数据库中可以避免 SAP 系统的内存错误。对于低容量或偶尔访问频率,直接从 PowerBI 读取作为 OData 服务可能效果也不错。您的环境的正确答案将有所不同。

第一步是定义数据库结构。这很容易,因为我们可以通过在服务网址末尾添加**/$metadata/**来查询我们服务的元数据。就像这样…

https://[YOUR_SAP_HOST_NAME]/sap/opu/odata/sap/ZGMF_SALES_SRV/$metadata/

<entitytype> 标签将包含我们创建 SQL 数据库所需的所有数据定义:

<edmx:Edmx xmlns:edmx="http://schemas.microsoft.com/ado/2007/06/edmx"
xmlns:m="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
  xmlns:sap="http://www.sap.com/Pro
tocols/SAPData" Version="1.0">
 <edmx:DataServices m:DataServiceVersion="2.0">
 <Schema xmlns="http://schemas.microsoft.com/ado/2008/09/edm" 
   Namespace="YGMF_SALES_SRV" xml:lang="en"
sap:schema-version="0">
 <EntityType Name="SalesOrder" sap:content-version="1">
 <Key>
 <PropertyRef Name="Vbeln"/>
 <PropertyRef Name="Posnr"/>
 </Key>
 <Property Name="Vbeln" Type="Edm.String" Nullable="false" MaxLength="10" 
   sap:label="Sales Document" sap:creatable="false" sap:updatable="false" 
   sap:sortable="false"sap:filterable="false"/>
<Property Name="Erdat" Type="Edm.DateTime" Precision="7" sap:label="Created on" 
   sap:creatable="false"
sap:updatable="false" sap:sortable="false"sap:filterable="false"/>
<Property Name="Posnr" Type="Edm.String" Nullable="false" MaxLength="6" 
   sap:label="Item" sap:creatable="false" sap:updatable="false" 
   sap:sortable="false"sap:filterable="false"/>
<Property Name="Matnr" Type="Edm.String" MaxLength="18" 
sap:label="Material" sap:creatable="false"
sap:updatable="false" sap:sortable="false" sap:filterable="false"/>
<Property Name="Kwmeng" Type="Edm.Decimal" Precision="13" Scale="3" 
  sap:label="Quantity" sap:creatable="false" sap:updatable="false" 
  sap:sortable="false"sap:filterable="false"/>
<Property Name="Kunnr" Type="Edm.String" MaxLength="10" sap:label="Customer" 
   sap:creatable="false" sap:updatable="false" sap:sortable="false" 
   sap:filterable="false"/>
</EntityType>
<EntityContainer Name="YGMF_SALES1_SRV_Entities" 
   m:IsDefaultEntityContainer="true" sap:supported-formats="atom json">
<EntitySet Name="SalesOrderSet" EntityType="YGMF_SALES_SRV.SalesOrder" 
   sap:creatable="false" sap:updatable="false" sap:deletable="false" 
   sap:pageable="false"sap:content-version="1"/>
</EntityContainer>
<atom:link xmlns:atom="http://www.w3.org/2005/Atom" rel="self"
href="http://<host>:<port>/sap/opu/odata/sap/YGMF_SALES1_SRV/$metadata"/>
<atom:link xmlns:atom="http://www.w3.org/2005/Atom" rel="latest-version"
href="http://scsecccid.sces1.net:8001/sap/opu/odata/sap/
 YGMF_SALES1_SRV/$metadata"/>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
注意

要执行接下来的步骤,您需要 SQL、SQL Server Management Studio 和 Microsoft Visual Studio Community。如果您还没有 SQL Server,建议先使用 SQL Express⁴。安装过程有很多好的教程⁵。如果您还没有使用过 SQL Server Management Studio,现在就开始吧。安装 SQL Server Management Studio⁶ 并连接到您的 SQL Express⁷。最后,如果您还没有使用过 Visual Studio Community,我们羡慕您。这就像告诉朋友一部精彩电影,希望自己能够再次第一次观看。下载 Visual Studio Community⁸ 并安装它。如果您还没有使用这些工具,起初可能会感到有些困难,但请相信,这些是强大且有趣的工具,将改变您对数据和分析的看法。勇敢地去探索,不要回头,享受其中的乐趣。

SQL Server

在我们建立数据库之前,让我们谈谈我们的意图。我们希望以最简单和最轻松的方式从我们的后端系统加载数据。为此,我们首先将所有数据加载到一个仓库表中。该仓库的数据类型将是简单的 Unicode 字符串。然后数据移动到一个分段表中,其中数据定义更加精确。成功完成后,数据将使用 Upsert 命令加载到最终的市场表中。这将根据我们定义的键(销售订单和行项目)插入任何新记录,并覆盖任何现有记录。

存储数据在 SQL 中的数据流

图 5-35. 数据流程用于在 SQL 中存储数据

打开 SQL Server Management Studio,输入服务器名称和您的凭据,然后单击“连接”按钮(图 5-36)。

连接到您的服务器

图 5-36. 连接到您的服务器

右键单击“数据库”文件夹(图 5-37)并选择“新建数据库”(图 5-38)。

SQL 层次结构

图 5-37. SQL 层次结构

创建新数据库

图 5-38. 创建新数据库

输入数据库名称并单击“确定”按钮(图 5-39)。

输入 SQL 数据库名称

图 5-39. 输入 SQL 数据库名称

点击新创建的数据库,然后右键单击“表”文件夹。选择“新建 → 表”(图 5-40)。

创建新的 SQL 表

图 5-40. 创建新的 SQL 表

使用 Unicode 数据类型和充足长度输入表的详细信息(图 5-41)。

输入 SQL 表变量

图 5-41. 输入 SQL 表变量

点击“保存”按钮。

输入表的名称,表示它是存储库表。点击“OK”按钮(图 5-42)。

为 repo 命名 SQL 表

图 5-42. 为 repo 命名 SQL 表

重复此过程,创建另一个表,但这次确保类型准确定义,如图 5-43 所示。

为阶段标识 SQL 变量

图 5-43. 为阶段标识 SQL 变量

点击“保存”按钮。

输入表的名称,表示它是阶段表。点击“OK”按钮(图 5-44)。

为阶段命名 SQL 表

图 5-44. 为阶段命名 SQL 表

重复与阶段表相同的过程,但将其标记为 mart 表(图 5-45)。这将是我们从中读取数据进入 PowerBI 的位置。

输入 mart 的 SQL 表名

图 5-45. 输入 mart 的 SQL 表名

现在我们的数据库已经准备好,我们需要创建操作,从 SAP 加载数据到其中。

SQL Server Integration Services(SSIS)

首先,让我们打开 Visual Studio Community。

按照菜单路径“文件 → 新建 → 项目”(图 5-46)。

创建新的 SSIS 项目

图 5-46. 创建新的 SSIS 项目

这里是您需要按照的步骤(图 5-47):

  1. 打开“Business Intelligence”文件夹。

  2. 点击“Integration Services”。

  3. 在右侧面板上,右键单击“Integration Services Project”。

  4. 输入项目名称。

  5. 点击“OK”按钮。

启动 Integration Services 项目的步骤

图 5-47. 启动 Integration Services 项目的步骤

在“Solution Manager”面板中,右键单击“Connection Manager”,然后选择“New Connection”。这是我们将定义到 SAP 的 OData 模型的连接位置。连接管理器默认具有一个 ODATA 选项(图 5-48)。

在 SSIS 中添加新连接

图 5-48. 在 SSIS 中添加新连接

选择我们将从 NWG(NetWeaver Gateway)读取的连接类型。这是一个 OData 连接;因此,点击 ODATA,然后点击“添加”按钮。然后按照以下步骤操作(图 5-49):

  1. 输入一个有意义的连接管理器名称。

  2. 输入服务文档位置,即我们从 SAP 获取的 NWG 数据源的 URL。确保使用服务而不是集合。

  3. 将身份验证类型更改为基本身份验证。

  4. 输入授权用户名。

  5. 输入密码。

在 SSIS 中的 SAP 网关服务连接设置

第 5-49 图。SAP 网关服务的 SSIS 连接设置

点击“测试连接”按钮以确保所有设置都正确。然后您应该会看到显示在 图 5-50 中的对话框。

从 SSIS 到 SAP 的连接测试

第 5-50 图。从 SSIS 到 SAP 的连接测试

点击“确定”按钮,然后再次点击“确定”以保存连接。接下来,我们需要定义读取过程的工作流程。在 SSIS 工具箱中点击“数据流任务”,并将其拖到控制流面板上(图 5-51)。

创建数据流任务

第 5-51 图。创建数据流任务

重命名数据流任务(图 5-52)。

重命名数据流任务

第 5-52 图。重命名数据流任务

双击数据流任务以导航到控制流或点击控制流选项卡。从公共部分拖动一个 OData 源组件。将其重命名为有意义的名称(图 5-53)。

在数据流中创建 OData 源连接

第 5-53 图。在数据流中创建 OData 源连接
  1. 双击 OData 组件。

  2. 选择之前创建的连接。

  3. NWG 服务的集合或实体集将显示。

  4. 点击“预览”按钮(图 5-54)。

SAP 连接的 OData 源设置

第 5-54 图。SAP 连接的 OData 源设置

将显示一个小预览(图 5-55)。如果一切正常,关闭预览窗口并点击“确定”按钮。

在 SSIS 中预览来自 SAP 的数据

第 5-55 图。在 SSIS 中预览来自 SAP 的数据

我们可以从 SAP 读取数据,现在我们需要将其放入我们之前创建的 SQL 数据库中。

在解决方案管理器面板中,右键单击连接管理器,选择“新建连接管理器”(图 5-56)。这是我们将定义到 SQL 数据库的连接的地方。

从 SSIS 添加到 SQL 的连接

第 5-56 图。从 SSIS 添加到 SQL 的连接

从列表中选择 OLEDB,然后点击“添加”按钮(图 5-57)。

选择 SQL 的 OLEDB 连接类型

第 5-57 图。选择 SQL 的 OLEDB 连接类型

点击“新建”按钮。输入 SQL Server 名称、认证和数据库名称。然后点击“测试连接”按钮(图 5-58)。

SSIS 中的 SQL 数据库连接设置

第 5-58 图。SSIS 中的 SQL 数据库连接设置

如果一切顺利,测试结果将是积极的(图 5-59)。

测试连接到 SQL

第 5-59 图。测试连接到 SQL

点击三次确定按钮以保存。

连接管理器窗格现在显示我们创建的两个连接(图 5-60)。

连接到不同系统的连接

第 5-60 图。连接到不同系统的连接

下一步是将 SAP 连接到 SQL 连接。选择 OLE DB 目标组件并将其拖入数据流选项卡中。给它起一个有意义的名称(图 5-61)。

在 SSIS 中创建 OLE DB 目标

第 5-61 图。在 SSIS 中创建 OLE DB 目标

用蓝色箭头将 SAP 组件连接到 SQL 组件(图 5-62)。红色箭头表示错误。

在 SSIS 中连接 SAP 和 SQL

第 5-62 图。在 SSIS 中连接 SAP 和 SQL

双击 SQL 组件。它会自动连接到 SQL 连接,因为只有一个选项。如果有多个选项,请从 OLE DB 连接管理器中选择适当的选项。从 SQL 数据库中选择存储库表(图 5-63)。

连接到特定的 SQL 数据库表

第 5-63 图。连接到特定的 SQL 数据库表

点击左侧的映射选项。如果数据库中的名称与 SAP 提供的名称相同,则条目将自动映射。如果不同,请手动映射。然后点击确定按钮(图 5-64)。

从 SAP 源映射到 SQL 数据库目标

第 5-64 图。从 SAP 源映射到 SQL 数据库目标

数据流现在应该不显示任何错误(图 5-65)。

当前映射中没有错误

第 5-65 图。当前映射中没有错误

点击保存按钮,然后点击开始按钮来测试流程。当完成时,会有两个绿色勾号(一个表示从 SAP 成功读取,一个表示成功写入 SQL 存储库表),如 图 5-66 所示。

在 SSIS 中执行 SAP 到 SQL 数据库流

第 5-66 图。在 SSIS 中执行 SAP 到 SQL 数据库流

确保数据存在于我们的数据库中。打开 SQL Server Management Studio 并导航到存储库表。右键单击表并从上下文菜单中选择“选择前 1000 行”(图 5-67)。

检查 SQL 数据库

第 5-67 图。检查 SQL 数据库

现在,来自 SAP 的结果已经在数据库资源库表中(图 5-68)。

通过 SSIS,SAP 数据现在在 SQL 中

图 5-68. 通过 SSIS,SAP 数据现在在 SQL 中

为什么我们要创建另外两个表?我们只是简单地将数据加载到资源库表中而已,不进行任何规则检查。这意味着我们不会检查整数是否为整数,日期是否为日期,或任何其他验证。回想我们在 图 5-35 中的流程。如果有数据错误,它们将在移至阶段时被捕捉到。由于我们使用了大型 Unicode 字符串,错误风险很小。现在我们将数据从资源库移至阶段表。

在 Visual Studio 中,点击“停止”按钮结束测试。在 SSIS 工具箱中,将执行 SQL 任务拖到控制流程选项卡,并为其取一个有意义的名称(图 5-69)。

在 SSIS 中创建一个执行 SQL 任务

图 5-69. 在 SSIS 中创建一个执行 SQL 任务

双击 SQL 任务以打开其属性。确保连接指向 SQL Server。点击省略号按钮以打开 SQL 编辑器(图 5-70)。

在 SSIS 任务中打开 SQL 编辑器

图 5-70. 在 SSIS 任务中打开 SQL 编辑器

输入 SQL 语句,将所有数据从资源库表移动到阶段表。然后连续点击两次 OK 按钮(图 5-71)。

从资源库表移动数据到阶段表的 SQL 代码

图 5-71. 从资源库表移动数据到阶段表的 SQL 代码

右键单击“执行 SQL 任务”,选择“执行任务”进行测试。任务成功完成后,SQL 数据库的阶段表将包含数据(图 5-72)。

在 SSIS 中执行单个任务

图 5-72. 在 SSIS 中执行单个任务

最后,我们将数据从阶段移至数据仓库。在控制流程选项卡中再次拖动另一个执行 SQL 任务。给它一个有意义的名称(图 5-73)。

在工作流程中添加另一个执行 SQL 任务

图 5-73. 在工作流程中添加另一个执行 SQL 任务

就像我们之前从资源库移至阶段的操作一样,双击 SQL 任务以打开其属性。确保连接指向 SQL Server。点击省略号按钮以打开 SQL 编辑器。

这里的 SQL 代码有些不同。它使用基于销售订单和项目的 MERGE 语句。如果数据仓库表中已有这个销售订单和项目,它将进行更新;否则将进行插入。完成后,连续点击两次 OK 按钮(图 5-74)。

从阶段表移动数据到数据仓库表的 SQL 代码

图 5-74. 从阶段表移动数据到 MART 表的 SQL 代码

正如之前所做的那样,右键单击执行 SQL 任务,然后选择执行任务进行测试。成功完成此任务后,SQL 数据库中的 MART 表中将有数据。

这里的概念是将数据倒入存储库表,插入到阶段表,然后合并到 MART 表中。为了使此流程正常工作,我们必须在成功加载到阶段表后清理存储库表,在成功加载到 MART 表后清理阶段表。通过不直接写入 MART 表,我们确保了安全的过程,并减少了意外损坏 MART 表的风险。

将两个新的执行 SQL 任务复制到控制流面板。给它们指示它们要执行的操作名称。我们将在数据成功移动后截断或删除存储库和阶段表(图 5-75)。

向 SQL 任务添加删除以前表的代码

图 5-75. 向 SQL 任务添加删除以前表的代码

就像之前做过的那样,将正确的连接分配给 SQL 数据库并打开 SQL 编辑器。对于 Truncate Repo 任务,输入以下简单的代码:

`TRUNCATE` `TABLE` SalesOrders_Repo;

对于 Truncate Stage 任务,输入以下内容:

`TRUNCATE` `TABLE` SalesOrder_Stage;

测试它们,然后在 SQL Server Management Studio 中检查确保存储库和阶段表为空。

每个组件目前都是独立的。将它们连接起来以创建工作流程(图 5-76)。如果其中任何步骤失败,流程将在那里停止,而不会继续——这正是我们想要的!再次参考 图 5-35。我们希望保护我们的 MART,以便在任何过程中的错误发生时停止整个流程,以免数据转移到 MART 之前。

注意

有些人可能认为整个存储库 → 阶段 → MART 过程增加了不必要的复杂性。如果您明智地监控数据并在数据加载时谨慎,您可以直接加载到 MART。然而,最*在更新一个超过 7000 万行的制药分析数据库时,我们的流程在插入到阶段时失败了。源数据结构已经不知不觉地发生了变化。存储库 → 阶段 → MART 设计为我们节省了一次重建,大量时间,也许更重要的是,避免了任何系统停机。

将任务连接到单个工作流程

图 5-76. 将任务连接到单个工作流程

点击保存按钮,然后点击开始按钮来测试该流程。如果所有组件成功完成,它们都将显示绿色勾号(图 5-77)。

执行整个工作流程并检查状态

图 5-77. 执行整个工作流程并检查状态

我们已经通过 NetWeaver Gateway 从 SAP 中提取数据的过程已经完成。我们使用了一个名为 SSIS 的提取工具自动化这一过程,并将数据拉入 SQL 数据库。现在这些数据已经可以用于高级分析和机器学*!

寻找异常

为了进行高级分析和机器学*工作,我们将使用 PowerBI 和 R,之前介绍过的 anomalize 包。为了获得技术上的乐趣,我们也将使用 PowerBI 和 Python 来做同样的事情——展示这两种语言及 PowerBI 工具本身的一些关键能力。Greg 是 R 的爱好者,而 Paul 则在 Python 上“胡来”。PowerBI 是将这些喜好融合起来的完美场所。

PowerBI 和 R

让我们了解数据科学家工具箱中另一个强大的工具……PowerBI。那么 PowerBI 到底是什么呢?从技术角度来看,PowerBI 是数据与展示之间的抽象层。您可以在向用户呈现数据之前对其进行建模和转换。此外,您可以使用 PowerBI 将不同的数据源合并和修改为一个报告。我们来为我们的 SAP 用户举个例子。您知道那个无处不在的 ALV(ABAP List Viewer)报告吗?那是数据和报告之间的抽象。PowerBI 比那个强大数百倍。对于熟悉商业智能模型的人来说,PowerBI 在实际报告之前提供了一个 ETL(提取-转换-加载)层。

首先,下载并安装 PowerBI。¹⁰ 要在 PowerBI 中使用 R,我们需要设置连接到我们的 SQL mart。打开 PowerBI 并点击“获取数据”按钮。然后选择“数据库”选项并突出显示 SQL Server 数据库。点击“连接”按钮(图 5-78)。

连接 PowerBI 到 SQL 数据库

图 5-78. 连接 PowerBI 到 SQL 数据库

输入服务器和数据库名称,然后点击“确定”按钮(图 5-79)。

在 PowerBI 中命名 SQL 数据库连接

图 5-79. 在 PowerBI 中命名 SQL 数据库连接

接受授权设置或根据需要更改它们(如果不使用 Windows 凭据)。点击“连接”按钮(图 5-80)。

连接到 PowerBI 中 SQL 数据库的授权

图 5-80. 连接到 PowerBI 中 SQL 数据库的授权

PowerBI 将通知您,它首先尝试了加密连接但未成功,现在将使用非加密连接。点击“确定”按钮(图 5-81)。

PowerBI 中用于加密支持的警告消息

图 5-81. PowerBI 中用于加密支持的警告消息

从 SQL 数据库中选择 mart 表。预览将显示在右侧面板中。点击“加载”按钮(图 5-82)。

在 PowerBI 中预览 SQL 数据

图 5-82. 预览在 PowerBI 中的 SQL 数据

当 PowerBI 加载数据时,它将显示如下对话框(见图 5-83 #powerbi_loading_indicator)。

PowerBI 加载指示器

图 5-83. PowerBI 加载指示器

数据加载完成后,PowerBI 将显示一个空白画布,包含用于可视化的工具和来自 SQL 数据库的字段。首先点击切片器按钮(见图 5-84 #selecting_the_slicer_visualization_in_po)。这使我们能够在 PowerBI 报告中动态过滤数据。

选择切片器可视化在 PowerBI 中

图 5-84. 在 PowerBI 中选择切片器可视化

然后点击客户字段以将其分配给切片器。画布上的切片器显示了分配情况。如果希望切片器具有“可搜索”功能,请点击右上角的省略号并选择“搜索”,如图 5-85 #using_the_searchable_option_for_the_slic 和 5-86 #the_searchable_option_in_the_slicer_visu 所示。

在 PowerBI 中使用可搜索选项的切片器可视化

图 5-85. 在 PowerBI 中使用可搜索选项的切片器可视化

在切片器可视化中使用可搜索选项

图 5-86. 切片器可视化中的可搜索选项

重复此过程以处理材料和日期(见图 5-87 #adding_other_slicer_visuals_in_powerbi)。

在 PowerBI 中添加其他切片器可视化

图 5-87. 在 PowerBI 中添加其他切片器可视化

我们的第一个可视化是一个简单的线图,显示按日期销售的材料。点击线图按钮并将其放置在画布上。如图 5-88 #adding_variables_to_the_line_chart_visua 所示,将日期分配到轴上,数量分配到值上,并将材料分配到图例中。然后添加所需的工具提示。

向线图可视化中添加变量

图 5-88. 向线图可视化中添加变量

由于尚未进行过滤,线图非常繁忙。我们已为报表创建了一个切片器,但尚未为其添加任何过滤条件(见图 5-89 #preview_of_the_line_chart_visual_in_powe)。

在 PowerBI 中线图可视化的预览

图 5-89. 在 PowerBI 中线图可视化的预览

接下来的步骤是添加一个 R 可视化。点击 R 按钮,并将其放置在线图下的画布上。将要使用的变量拖放到 R 可视化中。这些变量是日期和数量(见图 5-90 #using_the_r_visual_in_powerbi)。

在 PowerBI 中使用 R 可视化

图 5-90. 在 PowerBI 中使用 R 可视化

在脚本编辑器中,所选字段将显示为数据集的一部分(见图 5-91 #selecting_variables_for_the_dataset_in_r)。

在 PowerBI 中选择 R 中数据集的变量

图 5-91. 在 PowerBI 中选择数据集中的变量

将以下 R 代码放入脚本编辑器中(代码中的注释以井号(#)开头,描述下一行代码的功能):

#anomalize package by Matt Dancho and Davis Vaughan @business-science.io 
library(anomalize)
#tidyverse package by Hadley Wickham @ RStudio.com
library(tidyverse)
#tibble time package by Matt Dancho and Davis Vaughan @business-science.io
library(tibbletime)

#make sure that R sees the Date as a date variable
dataset$Date <- as.Date(dataset$Date)

#convert the dataframe to a tibble, which is still a dataframe but
#with tweaks to old behavior to make life easier
dataset <- as_tbl_time(dataset, index = Date)

#identify the dataset to be used 
#Reference 
#https://cran.r-project.org/web/packages/magrittr/vignettes/magrittr.html 
dataset %>%                                                                                              

  as_period("weekly") %>%       #set the period to daily 
  time_decompose(Quantity) %>%  #generate a time series decomposition
  anomalize(remainder) %>%      #detect outliers in a distribution
  time_recompose() %>%          #generate bands around the normal levels
  plot_anomalies(time_recomposed = TRUE) +
    ggtitle("Anomalies")      #plot the findings with the time_recompose

然后按播放按钮查看其工作效果。

起初并不太有意义,因为它正在评估所有客户和所有可用材料的异常。它尚未进行过滤(图 5-92)。

PowerBI 中 R 和线性视图的未过滤视图

图 5-92. PowerBI 中 R 和线性视图的未过滤视图

选择一个材料,以查看该特定材料下所有客户的异常情况(图 5-93)。

通过切片器过滤线性和 R 视图

图 5-93. 通过切片器过滤线性和 R 视图

首先要注意的是,线性图并不直观地显示异常。人们可能会认为峰值是异常,但 anomalize 包并不总是将它们分类为异常。现在点击一个客户以进一步细分值,显示特定客户和材料组合的异常(图 5-94 和 5-95)。

按材料和客户子集化视图以进行准确的异常检测

图 5-94. 按材料和客户子集化视图以进行准确的异常检测

异常现在更符合线性图,并且大的峰值确实显示为异常,但不是所有的异常。

仅查看 PowerBI 报告中图表的*距离视图

图 5-95. 仅查看 PowerBI 报告中图表的*距离视图

PowerBI 和 Python

无论您是 SAP 分析师还是数据科学家,每个处理代码的人都有自己偏好的语言、语法、样式和平台。在设计 PowerBI 时,微软深知这一点。前文设置并分析了销售订单数据,寻找 R 中的异常情况,但让我们跳到 Python。一些包帮助我们使这个过程更加高效:

luminol

一个设计用于时间序列数据分析的包。这是 LinkedIn 团队开源的。考虑到 LinkedIn 处理的大量数据以及其在产品价值中的相对重要性,可以想象他们的数据科学团队非常出色。这个包通过提供易于使用的 API 来检测异常,并调整检测的工作方式,证明了这一点。

Matplotlib

一个专为二维绘图设计的包。非常强大、功能丰富,在 Python 社区中很常见。我们将使用这个库来可视化我们的结果。

我们甚至可以在同一工具中保留我们的语言偏好,因为 PowerBI 内置支持使用 Python 输出图表和图形,允许我们使用这些库。这种支持目前处于预览状态,所以让我们来设置一下。毕竟,数据科学的一个重要部分就是敢于尝试新事物!

为了确保您的计算机可以运行新的 Python 功能,请安装最新版本的 Python,并使用以下三个pip命令安装我们将使用的包:¹¹

$ pip install pandas
$ pip install luminol
$ pip install matplotlib

接下来,打开 PowerBI,并使用之前用于 R 分析的同一文件。导航至设置菜单,在全局部分的预览功能菜单中,您可以添加尚未包含在当前完整发布版中的即将推出的功能(图 5-96)。

启用 PowerBI 中的 Python 支持

图 5-96. 启用 PowerBI 中的 Python 支持

选中 Python 支持旁边的复选框,并关闭选项菜单。您现在可以在此文档中使用 Python 了。我们将在我们之前创建的 R 图形的下方添加一个 Python 图形。在可视化菜单中,选择 Py 以向仪表板添加另一个分析部分(图 5-97)。

PowerBI 中的 Python 可视化

图 5-97. PowerBI 中的 Python 可视化

对于 PowerBI 的最后一点设置,请确保此可视化的字段属性与我们之前设置的 R 可视化的属性匹配(图 5-98)。

PowerBI 中 Python 可视化的选择字段

图 5-98. Python 可视化的选择字段

当 PowerBI 设置好并安装好所需的包后,我们准备好了。点击 Python 分析部分,并输入以下代码:

from luminol.anomaly_detector import AnomalyDetector
import matplotlib.pylab as plt 
import matplotlib.ticker as plticker 
# Helper function to make the raw dates into numbers for 
# luminol to interpret 
def make_date_int(date): 
    date_parts = date.split('-') 
    year = int(date_parts[0]) * 10000 
    month = int(date_parts[1]) * 100 
    day = int(date_parts[2][:2]) 
    return year + month + day 
# PowerBI sends the dataset as a dataframe with the field values 
# as individual lists. 
dataset_parts = dataset.to_dict('list') 
# Create a list of integer-ified dates, then make a dictionary with 
# keys as those integer-ified dates and values as the Quantity 
dates_to_int = list(map(make_date_int, dataset_parts['Date'])) 
data_for_detection = dict(zip(dates_to_int, dataset_parts['Quantity'])) 
# Keep a copy of the data with original dates on hand, zipped up nice 
base_preserved_dates = dict(zip(dataset_parts['Date'],  
                                dataset_parts['Quantity'])) 
anomalies = AnomalyDetector(time_series=data_for_detection,  
                            score_threshold=2,  
                            algorithm_name='exp_avg_detector' 
                           ).get_anomalies() 
# Extract out the dates that the AnomalyDetector found. 
# (List comprehensions are the best) 
anomaly_dates = [int(x.start_timestamp) for x in anomalies] 
# Here's where we set up a plot. 
ordered_data = sorted(base_preserved_dates.items()) 
xaxis, yaxis = zip(*ordered_data) 
fig, ax = plt.subplots() 
# Plot all the data, and then loop on the anomaly data to add 
# markers to the graph. 
plt.plot(xaxis, yaxis) 
for date in anomaly_dates: 
    highlight = data_for_detection[date] 
    timestamp_str = str(date) 
    timestamp = timestamp_str[:4] + '-' + timestamp_str[4:6] +
    '-' + timestamp_str[6:8] + 'T00:00:00.0000000' 
    plt.plot(timestamp, highlight, 'ro') 
# Showing the plot with x-axes ticks every 25 data points, 
# and makes the data nicely readable in '2018-09-12' date format. 
loc = plticker.MultipleLocator(base=25.0) 
ax.xaxis.set_major_locator(loc) 
ax.get_xaxis().set_major_formatter( 
    plticker.FuncFormatter(lambda x, p: xaxis[int(x)][:10] if int(x) < 
                           len(xaxis) else "")) 
plt.show()

在代码控制台中点击播放按钮,观看图表生成(图 5-99)。

如果将此图与从 R 代码生成的图进行比较,您会发现两个软件包都同意将 2017 年 1 月底的突发事件以及 2016 年 7 月的突发事件标记为异常。R 图捕获了 2017 年晚些时候的几个未被luminol库标记为可疑的点。

通过两次分析提出您的问题有两个显著优点。首先,您有机会学*新的方法和工具。SAP 分析师和数据科学家都可以肯定地看到扩展其工具集的价值。如今的技术专业人员不容忽视这一点。

PowerBI 中 Python 可视化中的异常

图 5-99. PowerBI 中 Python 可视化中的异常

第二,对于我们的数据分析目的来说最重要的是,你可以使用多种方法来调整你对数据提出的问题的答案。例如,注意 Python 代码中AnomalyDetector构造函数接受score_threshold参数。通过将score_threshold值设置为 2,我们将算法设置为仅解析某些异常。在调整该参数时,请注意在这种情况下将参数设置为较低或较高的数字将开始揭示数据下限存在的异常。

在监管部门的 Janine 和 SAP 分析师 Duane 需要问自己:对于分析目的和满足监管要求来说,异常低订单是否值得关注?相反,对于高数值阈值,我对这位客户了解多少?他们是否订购了超过仓库容量的产品?这是否可疑?许多 ERP 和 EDI 系统可以存储有关客户设施的信息。

总结

在本章中,数据科学团队完成了从 SAP 提取销售数据,存储到 SQL,并借助 R 或 Python 在 PowerBI 仪表板中显示结果的过程。也许看起来需要很多步骤才能使其工作,但一个人可以跟随这些步骤,在一天之内从 SAP 创建一个销售订单异常仪表板。对于试图赶上业务各部门数百个类似请求的大型数据仓库团队来说,同样的过程可能需要数周甚至数月的时间。我们已经能够在一天内为 SAP 销售订单原型化一个异常检测系统。这展示了在现代工具应用于业务数据时,数据科学的速度和能力。

通过在 R(anomalize)和 Python(luminol)中使用现成的库,我们发现可以快速轻松地检测数据中的异常。我们仅仅触及到了异常检测的表面,但已经暴露出了利用 SAP 业务数据进行此操作的可能性。本章学到的过程远不止异常检测。通过 NetWeaver Gateway 从 SAP 提取数据,再通过 SSIS 存储到 SQL 中,这是所有类型数据科学工作中常见且实用的过程。在我们的例子中,并不一定要将数据存储在 SAP 之外;然而,如果你发现自己在处理 SAP 财务数据时,这种技术可能会有所帮助。SAP 中的财务数据通常数量庞大,常常需要在分析之前提取出来。

虽然我们有一个良好的开始,这可能并非是我们在检测销售订单异常方面的终点。我们已向业务提供了概念验证和原型。监管部门的珍妮已经知道规定要求保持符合。然而,现在她了解到 IT 部门可以提供何种类型的分析和数据科学。这个原型将经过几次迭代和完善,最终成为一个强大的仪表盘,将保护珍妮及其公司免受不符合政府标准的处罚。这个项目本可以为其他公司服务得很好——那些遭到政府审查并最终被罚款的公司。

¹ 请参阅 Chapter 2 以了解监督学*和无监督学*的复*。

² 更多关于通用 ESD 如何运作的详细机制可以在https://www.itl.nist.gov/div898/handbook/eda/section3/eda35h3.htm找到。

³ 可以在Business Science网站上找到关于这个包的精彩介绍:https://www.business-science.io/code-tools/2018/04/08/introducing-anomalize.html

⁴ 可以在这里下载 SQL Server Express:www.microsoft.com/en-us/sql-server/sql-server-editions-express

⁵ SQL Express 的安装教程请参考https://www.sqlshack.com/install-microsoft-sql-server-express-localdb/

⁶ SQL Server Management Studio 下载地址:https://docs.microsoft.com/en-us/sql/ssms/download-sql-server-management-studio-ssms?view=sql-server-2017

⁷ 请参阅 https://docs.microsoft.com/en-us/sql/relational-databases/lesson-1-connecting-to-the-database-engine?view=sql-server-2017 将 SSMS 连接到 SQL。

⁸ Visual Studio Community 下载地址:https://visualstudio.microsoft.com/downloads/

⁹ 依我们的谦虚意见,像 PowerBI 和 Tableau 这样具有 ETL 层的工具是传统数据仓库模型的丧钟。

¹⁰ PowerBI 可从https://powerbi.microsoft.com/en-us/downloads/下载。

¹¹ Python 中安装新包的方式有很多。在我们的例子中,我们使用了基本的命令行。

第六章:R 和 Python 中的预测分析

Big Bonanza Warehouse 的团队在销售预测方面遇到了一些问题,销售副总裁转向作为销售和分销分析师工作的 Duane 寻求帮助。每季度他们大约收集一次数据并将其发送给外部公司进行处理。结果是预测他们未来几个季度所有产品的销售,但他们发现为他们生成的预测太过一般化(基于季度),并且经常是极不准确的。他们是否可以得到一些帮助他们了解下周销售可能是什么的东西?简言之,他们想要对畅销产品的销售进行每周预测。

Duane 有一些想法。他曾与公司的数据科学家 Greg 和 Paul 合作过,对数据科学有一些了解。一段时间内产品的销售是一个时间序列¹问题。他们有足够的历史数据来尝试寻找模式。这不是纯粹的预测,而是模式检测。这是销售团队可以使用的东西,而不是他们的直觉感觉。Duane 决定使用一些预测分析。借助正确的 R 或 Python 工具集和一些预测分析的前期知识,Duane 不需要花几个月时间支付昂贵的顾问费用来建立庞大的数据湖。他可以动手找到答案。

数据科学中有许多术语很模糊(包括数据科学本身!),但预测分析占据了特殊的不可理解性份额。您可能已经听说了那个如今臭名昭著的“预测”故事。零售巨头 Target 在父亲甚至不知道之前预测了一个少女的怀孕。如果您还没有听说这个故事…… Target 开始向一个少女发送婴儿优惠券。她的父亲抱怨说他们在鼓励他的女儿怀孕。事实是,这个女孩已经怀孕了。Target 是否预测了这个少女的怀孕?答案是否定的。这不是预测;这是推断或分类。他们使用的特征是客户的购物行为。这个少女的购买*惯导致分类算法将她归类为“怀孕”类别。

这个故事的要点是突出显示“预测”一词的两种常见用法。其中一种预测是统计推断,另一种是预测。让我们在这里诚实一点,您可以对这些定义进行无休止的辩论。对于我们的目的,我们将在预测和推断之间划清界限。对我们来说,预测将是预测。² 让我们确保我们对术语的理解是一致的:预测是预测未来的行为

确定分析是否属于预测的试金石是询问:“事件是否已发生?”如果是,那么根据我们的定义,它很可能不是预测。

以下是数据科学学*材料中常见的被误标记为预测的主题和练*的一些示例:

预测波士顿房价

这是另一个基于给定物业的特征(如位置、面积、卧室数量等)的分类/推理问题。

预测 泰坦尼克号 的生存率

这是另一个基于性别、客舱号码(位置)、家庭成员数量、出发点等特征的分类/推理问题。

预测欺诈信用卡行为

这是一个异常检测问题,确定持卡人的行为是否在容忍范围内。更具描述性的称呼可能是“检测欺诈信用卡行为”。

预测为什么和何时患者会再次收治

这听起来非常接*预测。这是另一个基于已被再次收治的患者的特征进行分类/推理的问题。如果他们的特征与尚未再次收治的患者的特征匹配,他们可能会再次收治。

一些被正确标记为预测的主题和练*的示例:

预测波士顿房价 明年

明年的房屋价值是基于某些特征对当前房屋价值的分类。这一分类与其他显著数据(如 GDP)融合,以进行未来预测。

预测未来股票价值

你来解决这个问题……告诉我们。许多不同的数据源有助于预测股票的表现。公司在 EDGAR³ 的季度业绩报告是一个很好的起点。

预测销售在 R 中

在本章中,我们将详细介绍 Duane 在预测分析中的练*,并尝试进行销售订单预测。我们将按照 图 6-1 中的流程进行此任务。

数据分析与预测流程

图 6-1. 数据分析与预测流程

步骤 1: 确定数据

还不准备完全独立行动,Duane 向数据科学团队寻求更好的指标,以预测未来销售——接下来的几周、几个月、几个季度,以及一年内。随着预测准确度随着预测的时间越远而变得更加不稳定。简单地说,预测明天的销售要比预测明年的销售容易得多,因为我们知道昨天的销售情况。然而,并不是说我们可以预测明年的销售,因为我们知道去年的情况。我们知道的是,我们将会有一系列产品的销售数据。

步骤 2: 收集数据

我们的数据来源是 SAP。我们将使用与 第四章 中相同的方法提取数据。使用 ABAP QuickViewer 查询,我们从 VBAP 和 VBAK 表中收集简单的销售数据。我们只从 VBAK 中取 ERDAT(创建日期)。我们从 VBAP 表中取 MATNR(物料)和 KWMENG(销售数量)。

步骤 3: 探索数据

从 SAP 导出数据为 CSV 文件后,我们将其读入 R 进行查看:

sales <- read.csv('D:/DataScience/Data/Sales.csv')

让我们先看看前 10 行:

head(sales)

   X DailySales Material       ReqDeliveryDate
 1 0   48964.75     1234 /Date(1420416000000)/
 2 1   30853.88     1234 /Date(1420502400000)/
 3 2   65791.00     1234 /Date(1420588800000)/
 4 3   17651.20     1234 /Date(1420675200000)/
 5 4   36552.90     1234 /Date(1420761600000)/
 6 5    5061.00     1234 /Date(1420848000000)/

我们立即看到两件事。行进来时在列 X 下,我们不需要它。此外,日期字段看起来很奇怪,看起来像 UNIX 时间,我们需要进行转换。进一步查看后,我们发现它确实是 UNIX 时间,但末尾多余的三个零需要去掉。让我们在继续之前先纠正这些问题:

#Remove the X column
sales$X <- NULL
#Remove all nonnumeric from the date column
sales$ReqDeliveryDate <- gsub("[⁰-9]", "", sales$ReqDeliveryDate)
#Convert the unix time to a regular date time using the anytime library
library(anytime)
#First trim the whitespace
sales$ReqDeliveryDate <- trimws(sales$ReqDeliveryDate)
#Remove the final three numbers
sales$ReqDeliveryDate <- gsub('.{3}$', '', sales$ReqDeliveryDate)
#Convert the field to numeric
sales$ReqDeliveryDate <- as.numeric(sales$ReqDeliveryDate)
#Convert the unix time to a readable time
sales$ReqDeliveryDate <- anydate(sales$ReqDeliveryDate)

现在我们已经完成了一些操作,让我们来看看我们数据的结构。使用函数str()来查看数据的结构:

str(sales)
 'data.frame': 2359 obs. of 3 variables:
  $ DailySales  : num 48965 30854 65791 17651 36553 ...
  $ Material    : int 1234 1234 1234 1234 1234 1234 1234 1234 1234 1234 ...
  $ ReqDeliveryDate: Date, format: "2015-01-04" "2015-01-05" "2015-01-06"  ...

我们看到我们有一个包含三个变量的数据框,共有 2,359 个观测。让我们来找出数据框中材料的分布情况。使用ggplot2库中的以下命令(这会在图 6-2 中呈现):

ggplot(`sales`, aes(`Material`)) + geom_bar()

我们销售数据中材料的分布。

图 6-2. 我们销售数据中材料的分布

我们已经有了我们喜欢的数据格式。现在是时候绘制和探索数据了。

第四步:建模数据

我们将使用 R 中的ggplot2dplyrscales库来建模我们的数据。这些是 R 生态系统中最有用和多功能的包之一:

library(ggplot2)
library(dplyr)
require(scales)

首先,我们进行一些前期工作。我们希望我们的图表有漂亮的数字,所以我们使用来自scalesformat_format函数来定义这些内容。该函数的效果是简单地将我们的数字格式化,使小数点为句点,千位数为逗号,并且不使用科学计数法。

point <- format_format(big.mark = ",", decimal.mark = ".", scientific = FALSE)

让我们简单地绘制材料随时间的销售情况:

sales %>%
  ggplot(aes(x=ReqDeliveryDate, y=DailySales)) +
  geom_point(color = "darkorchid4") +
  scale_y_continuous(labels = point) +
  labs(title = "Sales over time",
       subtitle = "sales for all materials",
       y = "Sales Quantities",
       x = "Date") +
  theme_bw(base_size = 15)

用人类语言说,这段 R 代码的意思是:“取销售数据框并将其发送到ggplot。将 x 轴设为ReqDeliveryDate,y 轴设为DailySales。使用darchorchid4色调的点。然后将 y 轴格式化为point格式。最后,好好标注一切并给它一个基本的主题和大小。” 结果显示在图 6-3 中。

所有材料随时间的销售情况。

图 6-3. 所有材料随时间的销售情况

这给了我们关于销售随时间的分布的想法,但它混合了材料,并且点并不适合绘制时间序列。让我们分开材料,并选择线而不是点。我们在图 6-4 中看到了结果。

按颜色分开的材料随时间的销售情况。

图 6-4. 按颜色分开的材料随时间的销售情况

这样做更好了,但材料仍然混杂在一起不够清晰。也许我们需要完全分开它们。ggplot2facet_wrap可以很好地完成这项任务。以下代码生成了图 6-5 中的图表:

sales %>%
  ggplot(aes(x=ReqDeliveryDate, y=DailySales, color)) +
  geom_line(color = "darkorchid4") +
  facet_wrap( ~ Material) +
  scale_y_continuous(labels = point) +
  labs(title = "Sales over time",
       subtitle = "sales for all materials",
       y = "Sales Quantities",
       x = "Date") +
  theme_bw(base_size = 15)

销售材料随时间变化的图表,以便我们可以分开查看每个绘图

图 6-5. 销售材料随时间变化的分段图表

现在我们可以更轻松地看到每种材料的分布情况。我们可以快速发现其中两种材料最*才开始销售。

让我们专注于一个材料,1234。我们将使用geom_smooth函数添加一个简单线性模型。这里我们只有一个材料,但我们保留facet_wrap是因为它使得标题非常漂亮(结果显示在图 6-6 中):

sales %>%
  subset(Material == '1234') %>%
  ggplot(aes(x=ReqDeliveryDate, y=DailySales, color)) +
  geom_line(color = "darkorchid4") +
  facet_wrap( ~ Material ) +
  geom_smooth(method = "lm") +
  scale_y_continuous(labels = point) +
  labs(title = "Sales over time",
     subtitle = "sales for all materials",
     y = "Sales Quantities",
     x = "Date") +
  theme_bw(base_size = 15)

简单线性映射下的销售随时间变化

图 6-6. 简单线性映射下的销售随时间变化

我们想知道这些年度销售如何相互匹配。线性图表使得难以判断 2015 年的整体周销量是否高于 2018 年。

让我们放松心态,以直觉的方式来解决问题。在 R 编程中有许多重要概念,但其中两个最具影响力的是Tidy DataSplit-Apply-Combine

理解这些概念将帮助您更轻松地思考问题。我们将简单地将我们的sales数据框分割成一个更容易绘制的子数据框。首先,我们将我们的销售数据复制到一个仅包含我们材料的子数据框中:

sales_week <- sales %>% subset(Material == '1234')

其次,我们需要从日期变量中创建一个周变量和一个年变量。在 R 中,使用基本函数strftime很容易实现这一点:

sales_week$week <- strftime(sales_week$ReqDeliveryDate, format = '%V')
sales_week$year <- strftime(sales_week$ReqDeliveryDate, format = '%Y')

我们不再需要ReqDeliveryDateMaterial

sales_week$ReqDeliveryDate <- NULL
sales_week$Material <- NULL

我们还想将我们的周聚合到一个桶中。在我们的sales数据框中,可能会有给定周的多次销售,我们希望在sales_week数据框中只保留一个周的销售:

sales_week <- sales_week %>% group_by(year, week) %>% summarise_all(sum)

如果我们现在查看我们的数据框,它具有以下列:

 head(sales_week)
 # A tibble: 6 x 3
 # Groups:   year [1]
   year week DailySales
   <chr> <chr>      <dbl>
 1 2015 01        48965.
 2 2015 02       173920.
 3 2015 03       213616.
 4 2015 04       243433.
 5 2015 05       304793.
 6 2015 06       265335.

现在我们可以再次使用ggplot2来查看按年度比较周销量的情况(结果在图 6-7 中显示):

sales_week %>% 
  ggplot(`aes`(`x` = week, y = DailySales, group = year)) +
  geom_area(`aes`(`fill` = year), position = "stack") +
  labs(`title` = "Quantity Sold: Week Plot", x = "", y = "Sales") +
  scale_y_continuous() +
  theme_bw(`base_size` = 15)

按年度销售面积图

图 6-7. 按年度销售面积图

这张图告诉我们一些以前无法清晰看到的内容。每年销售量的峰值和谷底之间有着强烈的相关性。这种年度之间的强相关性让我们相信可以根据这种模式进行建模。2018 年没有完整记录的年度销售,所以该图表在第 29 周停止。我们还看到在第 35 至 40 周和第 4 至 9 周之间销售明显上升。

要进一步探索,我们需要访问 R 中的一个基本对象,即ts(时间序列)对象。它是一组随时间变化的数值数组。到目前为止,我们一直在使用的是数据框(dataframe)。在 R 中,将data.frame轻松转换为ts对象。此函数以数据本身作为第一个参数,然后是一系列其他参数,我们现在将逐一介绍。在控制台中键入**args(ts)**以查看基本ts函数的参数列表:

args(`ts`)
function (`data` = NA, start = 1, end = numeric(), frequency = 1,
    deltat = 1, ts.eps = getOption("ts.eps"), class = if (`nseries` >
        1) c("mts", "ts", "matrix") else "ts", 
        names = if (!is.null(`dimnames`(`data`))) 
        colnames(`data`) else paste("Series",
        seq(`nseries`)))

我们将使用的参数是:

  • startend参数定义了ts对象的起始日期和结束日期。

  • frequency参数指定了单位时间内的观察次数。

为了更轻松、更清晰地完成这一任务,我们需要重新格式化我们的数据框架。这将导致一个整洁美观的ts对象。

这一次我们将按月进行分析。就像我们按周进行分析一样,我们将子集化sales数据框架。不过,这一次我们会重新命名列,以便更容易使用和记忆:

sales_month <- sales %>% subset(Material == '1234')
 sales_month$Material <- NULL
 colnames(sales_month) <- c('sales', 'date')

另外,ts对象不喜欢间隙。如果你要按天分析数据,ts对象希望每天都有数据……即使某天没有销售数据。如果按分钟分析数据,同样每分钟都必须有数据。例如,如果某天没有销售某种材料,日期序列中就会出现间隙。我们想用 0 填补所有这些间隙,因为那天实际销售的数量就是 0。首先,我们创建一个新的数据框架,包含从sales_month的第一天开始到最后一天的所有可能日期:

all_dates = seq(`as`.Date(`min`(`sales_month`$date)),
                as.Date(`max`(`sales_month`$date)),
                by="day")

然后我们想将这个数据框架与sales_month合并:

sales_month <- merge(data.frame(date = all_dates),
                      sales_month,
                      all.x=T,
                      all.y=T)

让我们来看看我们的数据:

head(sales_month, n=10)
         date    sales
 1  2015-01-04 48964.75
 2  2015-01-05 30853.88
 3  2015-01-06 65791.00
 4  2015-01-07 17651.20
 5  2015-01-08 36552.90
 6  2015-01-09 5061.00
 7  2015-01-10       NA
 8  2015-01-11 18010.00
 9  2015-01-12 24015.00
 10 2015-01-13 39174.25

我们马上注意到我们的数据中有缺失值(NA)。这是在没有销售的那些天。让我们用 0 替换它们:

sales_month$sales[is.na(sales_month$sales)] = 0

现在创建一个ts对象就很容易了。坦率地说,在 R 中,ts对象一直是一个难题。听从我们的建议——花时间对数据框架进行清晰简洁的格式化,你将轻松通过ts对象部分:

require(xts)
sales_ts <- xts(sales_month$sales, order.by = as.Date(sales_month$date))

现在我们有了一个格式良好的ts对象,可以在其上做一些简单的图表:

plot(sales_ts)

ts 对象的简单图

图 6-8. ts对象的简单图

我们还可以轻松地查看每月的销售额和平均销售额。通过观察图 6-9,我们可以看到尽管有高峰和低谷,整体销售金额相对平均。

monthplot(sales_ts)

ts 对象的月度图

图 6-9. ts对象的月度图

预测图表

在预测分析中,我们通常要使用三种简单图形:均值、朴素和漂移。让我们分别来看看这些图形。

第一个图是对未来均值的简单预测。这一预测假设过去销售的平均值将继续保持。要创建这些图表,我们需要使用forecast⁴库。我们在图 6-10 中看到了均值预测的结果。

library(forecast)
sales_ts <- ts(sales_month$sales) 
sales_ts_mean = meanf(sales_ts,h=35,level=c(90,90), 
                      fan=FALSE, lambda = NULL)
plot(sales_ts_mean)

您可能会想知道图表(最右侧的灰色矩形区域)告诉您什么。灰色区域是置信区间,默认情况下为 95%。灰色区域中间的线表示预测应该落在哪里,灰色区域表示:“我有 95%的信心,如果值不在线上,它会在灰色区域内。”可以假设,95%的置信区间相当高,因此该区域必须足够大以确保这一点。

简单的均值预测及其置信区间

图 6-10. 简单的均值预测及其置信区间

天真的假设是销售将与最后一次观察相同,如图 6-11 所示:

sales_ts_naive <- naive(`sales_ts`,h=35,level=c(90,90),
                         fan=FALSE,lambda=NULL)
plot(`sales_ts_naive`)

简单的天真预测及其置信区间

图 6-11. 简单的天真预测及其置信区间

最后,我们可以很容易地通过forecast库查看图表的漂移。漂移从天真的起点开始,但然后根据数据的平均整体变化进行正向或负向调整。我们在图 6-12 中看到了结果。请注意,这里有一个细微的向下趋势:

sales_ts_drift <- rwf(sales_ts,h=35,drift=T,level=c(90,90),
                      fan=FALSE,lambda=NULL)
plot(sales_ts_drift)

简单的漂移预测及其置信区间

图 6-12. 简单的漂移预测及其置信区间

显然,这些图表并不能令销售副总裁满意,但这只是我们预测过程的开始,我们将继续改进这个过程,直到我们获得满意的结果。

第五步:评估模型

首先,让我们分析刚刚创建的三个图的准确性。使用forecast包很容易做到这一点,但在此之前,我们需要讨论如何衡量准确性。我们将分析六种方式来分析准确性,而表 6-1 展示了最常用的测量方法。

表 6-1. 准确性测量

测量 描述 符号
ME 平均误差:预测误差的平均数。正误差有可能抵消负误差。 mean(e[i])
MAE 平均绝对误差:预测误差的平均值。这不考虑误差是过高还是过低,只考虑误差的大小。 mean(|e[i]|)
RMSE 均方根误差:与 MAE 相同,但在总和开平方之前对误差进行了平方。这样做的结果是大误差比小误差更有价值。如果您希望严重惩罚大误差,可以考虑此改进。 SQRT(mean(e²[i]))
MPE 平均百分比误差:误差百分比的平均值。 mean((e[i] / actual[i]) *100)
MAPE 平均绝对百分比误差:误差的绝对百分比的平均值。 mean((|e[i] / actual[i]) *100|)
MASE 平均绝对标度误差:绝对值的标度^(a)误差的均值。标度是代替百分比误差的一种方法。MASE > 1 表明预测比天真预测更糟糕。如果 < 1 则更好。 mean(|q[i]|)
^(a) MASE 是由统计学家罗布·海德曼于 2005 年提出的,用于确定预测的比较准确性。

我们可以使用forecast包轻松查看这些值(见表 6-2):

accuracy(sales_ts_mean)
accuracy(sales_ts_naive)
accuracy(sales_ts_drift)

表 6-2. 均值、天真和漂移预测的准确性度量

ME RMSE MAE MPE MAPE MASE
Mean -2.46E-13 28535.31 23019.78 -Inf Inf 1
Naïve -33.81419 31653.02 23017.03 -Inf Inf 1
Drift 1.25E-12 31653 23021.87 NaN Inf 1.00021

对于这些评估,什么是好的值?请考虑每组数据都不同且具有不同的比例。一个实验中的数据可能在 1 到 1,000,000 的范围内,而 RMSE 为 10,这似乎非常好。然而,如果数据的范围是 1 到 20,则相同的 RMSE 值就很糟糕了。因此,考虑使用您的评估方法来比较不同的图表和测试,避免盲目地将小评估结果视为好的陷阱。在我们刚刚看过的简单方法中,均值似乎领先。我们的结果还向我们展示了使用百分比的一些风险。存在除以零或接*零的风险,导致无限值。

在我们的数据中,另一件需要关注的事情是季节性。这是一种通过使用tseries⁵库大大简化的探索数据的不同方式。是否有基于周期事件的某种模式?时间序列中季节性的一个示例是连指手套的销售。显然,连指手套的销售在冬季增加,在夏季减少。另一种说法是数据是否平稳或不平稳。平稳数据与实际时间序列无关。我们可以通过使用tseries包中的adf.test方法来测试平稳性或非平稳性:

library(tseries)
sales_ts_adf <- adf.test(sales_ts) 
sales_ts_adf

我们得到的结果清楚地表明数据是平稳的(没有季节性):

Augmented Dickey-Fuller Test
data: sales_ts[, 1]
Dickey-Fuller = -5.8711, Lag order = 10, p-value = 0.01
alternative hypothesis: stationary

现在是时候进行一些比均值、天真或漂移更好的预测了。我们将首先使用 ARIMA 模型。ARIMA⁶代表自回归积分移动平均。ARIMA 是一种根据其先前数据点预测(预测)未来值的技术。正如名称所示,它使用移动平均。首先,我们使用以下命令创建未来的值:

sales_future <- forecast(`auto`.arima(`sales_ts`))

为了更好地理解结果,让我们看一下我们正在处理的时间序列对象的结构。使用str()命令:

> str(sales_ts)
An 'xts' object on 2015-01-04/2018-07-20 containing:
  Data: num [1:1294, 1] 48965 30854 65791 17651 36553 ...
  Indexed by objects of class: [Date] TZ: UTC
  xts Attributes: NULL

这告诉我们,在我们的时间序列中有 1,294 个对象。我们的forecast函数将预测未来另外 10 个值。这给出了以下销售值和 80 和 95 的置信区间。让我们以时间序列对象 1300 为例。这告诉我们预测值为 20892.628,Lo 80 为 -8393.256,Hi 80 为 50178.51。这意味着我们有 80% 的置信度销售额将在 -8,393.256 到 50,178.51 美元之间。95% 的置信区间显然更宽,以应对更高的置信度,因此其范围更大,为 -23,896.27 到 65,681.52 美元:

      Point Forecast      Lo 80    Hi 80     Lo 95    Hi 95
 1295        907.887 -26883.351 28699.12 -41595.14 43410.92
 1296      11811.826 -16743.171 40366.82 -31859.27 55482.93
 1297      21790.271 -6768.050 50348.59 -21885.91 65466.45
 1298      23937.037 -5022.613 52896.69 -20352.92 68227.00
 1299      25546.677 -3730.630 54823.98 -19229.10 70322.46
 1300      20892.628 -8393.256 50178.51 -23896.27 65681.52
 1301      10542.993 -19636.125 40722.11 -35611.98 56697.97
 1302       5537.931 -27502.193 38578.06 -44992.58 56068.44
 1303      10655.408 -24005.266 45316.08 -42353.52 63664.34
 1304      19295.901 -15714.125 54305.93 -34247.31 72839.12

现在让我们绘制这张图表。这次我们将从点 750 到 1304 开始,这样我们就不会有这么小的预测间隔。我们只会绘制时间序列从点 750 到 1304. 在图 6-13 中的结果显示点值是黑线,80% 置信区间是浅色阴影,90% 置信区间是更浅的阴影:

plot(`sales_futue`, xlim = c(750,1304))

ARIMA 预测

图 6-13. ARIMA 预测

到目前为止,我们已经进行了一些预测,并且在这个过程中学到了很多。也许我们应该坐下来看看我们的数据,并想一想这个 ARIMA 是否已经足够好了?我们应该尝试一些新的东西吗?当您看到图 6-13 时,脑海中会想到什么?当我们看到它时,我们看到底部的平直线。那是我们输入缺失日期的地方,那些材料没有销售的日期。这些是否应该包含在进行预测的模型中?在许多情况下,答案可能是否定的。然而,在这里有一个很好的论点,即零销售也很重要。任何销售日都是一个数据点,任何非销售日也是如此。

接下来,我们将切换到另一种语言和不同的模型。我们将采用在 R 中探索过的同一系列数据,并在 Python 中执行相同的五步分析。

在 Python 中预测销售

有许多方法可以使用不同的工具(如 Python 和 R)分析数据。在本节中,我们将从 Python 的角度处理相同的数据。

第一步:识别数据

这次我们将使用一个快速的 OData 实用类,它可以加快未来识别和收集阶段的速度。我们将使用 OData 和 Python 来帮助预测未来几周、几个月甚至一年的销售情况。此识别阶段与我们之前在 R 中所做的相同。

第二步:收集数据

还记得第三章中我们为从 SAP 后端列出植物创建了一个示例 OData 服务的时候吗?我们将在这里完全做同样的事情——只是有些调整结构、字段,并加入少许 ABAP 代码以使拉取变得简单。最重要的是,以这种方式定义一个过程将使您能够轻松地以编程方式收集不同的材料和日期范围!我们将在此处突出显示与第三章中所述方法的主要区别。

首先,在事务 SE11 中创建一个结构,并从图 6-14 中选择字段进行填充。我们将其与之前的 R 示例具有相同的基本结构。

销售数据的 SE11 结构。

图 6-14. 销售数据的 SE11 结构

接下来,转到事务 SEGW 创建一个新的 OData 服务,并按图 6-15 中的项目详情输入。

从 SEGW 中的 OData 服务获取的详情。

图 6-15. 来自 SEGW 的 OData 服务详情

记住导入我们创建的结构,就像在第三章中一样。参见图 6-16、图 6-17 和图 6-18 以获取要使用的设置。

将 SE11 结构导入到新的 OData 服务中。

图 6-16. 将 SE11 结构导入到新的 OData 服务中

选择所有可用字段进行导入。

图 6-17. 选择所有可用字段进行导入

MATERIAL 和 REQ_DELIVERY_DATE 是关键字段。

图 6-18. MATERIAL 和 REQ_DELIVERY_DATE 是关键字段

再次,就像第三章一样,重新定义刚刚创建的实体的 GetEntitySet (Query) 方法。使用以下 ABAP 代码设置销售订单项数据的快速过滤操作。如前所述:本书不会深入解释 ABAP 代码。如果您是一位数据科学家,确实需要 SAP 数据但没有 SAP 团队帮助,那么您可能希望在 SAP 的免费培训网站https://open.sap.com上补充一些 ABAP 基础知识:

"This code will return a list of sales dollars by date per material. 
"The filtering mechanism for OData allows us to limit this to a subset 
"of materials, and the below Python code incorporates this feature.

"If you named your entity set differently than our example screenshots,
"this method will be named differently.
METHOD dailymaterialsal_get_entityset.
  DATA lr_matnr TYPE RANGE OF matnr.
  DATA ls_matnr `LIKE` LINE OF lr_matnr.
  DATA lr_vdatu TYPE RANGE OF edatu_vbak.
  DATA ls_vdatu `LIKE` LINE OF lr_vdatu.

   "Here we extract the filters that our Python code will insert.
  LOOP AT it_filter_select_options `INTO` DATA(ls_select).
    `IF` ls_select-property EQ 'Material'.
      LOOP AT ls_select-select_options `INTO` DATA(ls_option).
        MOVE-CORRESPONDING ls_option `TO` ls_matnr.
        ls_matnr-low = |{ ls_option-low ALPHA = `IN` }|.
        APPEND ls_matnr `TO` lr_matnr.
      ENDLOOP.
    ELSEIF ls_select-property EQ 'ReqDeliveryDate'.
      LOOP AT ls_select-select_options `INTO` ls_option.
        MOVE-CORRESPONDING ls_option `TO` ls_vdatu.
        ls_vdatu-low = |{ ls_option-low ALPHA = `IN` }|.
        APPEND ls_vdatu `TO` lr_vdatu.
      ENDLOOP.
    ENDIF.
  ENDLOOP.

  "This SELECT statement incorporates the filters that are sent by the
  "Python code below into the SQL logic. For example, if the programmer
  "enters 3 materials to filter, then the variable 'lr_matnr' contains
  "a reference to those 3 materials to pass to the database engine.
  `SELECT` item~matnr `AS` material
         head~vdatu `AS` req_delivery_date
         SUM( item~netpr ) `AS` daily_sales
    `FROM` vbak `AS` head
      `INNER` `JOIN` vbap `AS` item `ON` head~vbeln = item~vbeln
      `INNER` `JOIN` knvv `AS` cust `ON` head~kunnr = cust~kunnr
        `AND` head~vkorg = cust~vkorg
        `AND` head~vtweg = cust~vtweg
        `AND` head~spart = cust~spart
      `INNER` `JOIN` mara `AS` mtrl `ON` item~matnr = mtrl~matnr
    `INTO` CORRESPONDING FIELDS OF TABLE et_entityset
     `WHERE` head~vdatu `IN` lr_vdatu
       `AND` item~matnr `IN` lr_matnr
    `GROUP` `BY` item~matnr vdatu
    `HAVING` SUM( item~netpr ) > 0
    `ORDER` `BY` item~matnr vdatu.
ENDMETHOD.

完成并激活 SAP Gateway 代码后,您将拥有一个能够向任何能发出 OData 请求的客户端发送所需销售数据的服务。自然地,您希望将自己的笔记本电脑用作其中之一,因此我们设计了一个小的实用类,可以在您的本地计算机上执行一些基本的 OData 过滤、请求和创建 CSV 文件的操作。在许多 SAP 数据检索场景中,这可能非常有用:

# Utility is exposed as a class to be instantiated per request run
class GatewayRequest(object):
     def __init__(self, gateway_url='', service_name='', entity_set_name='',
                  user='', password=''):
        self.gateway_url = gateway_url.strip('/')
        self.service_name = service_name.strip('/')
        self.entity_set_name = entity_set_name.strip('/')
        self.filters = [] 

        # Basic authentication: a username and password base64 encoded 
        # and sent with the OData request. There are many flavors of 
        # authentication for available for OData - which is just a RESTful 
        # web service - but basic authentication is common inside corporate 
        # firewalls.
        self.set_basic_auth(user, password)

    # Adds a filter to the main set of filters, which means our OData 
    # utility can support multiple filters in one request.
    def add_filter(self, filter_field, filter_option, filter_value): 
        # OData supports logical operators like 'eq' for equals, 
        # 'ne' for does not equal, 'gt' for greater than, 'lt' for less 
        # than, 'le' for less than or equal, and 'ge' for greater than or 
        # equal. 'eq' is the most common, so if the logical operator is 
        # omitted we assume 'eq'
        if not filter_option:
            filter_option = 'eq'

        new_filter = [filter_field, filter_option, filter_value]
        self.filters.append(new_filter)

    # Encode the basic authentication parameters to send with the request. 
    def set_basic_auth(self, user, password):
        self.user = user
        self.password = password
        string_to_encode = user + ':' + password
        self.basic_auth =  
             base64.b64encode(string_to_encode.encode()).decode()

    # OData works through sending HTTP requests with particular query 
    # strings attached to the URL. This method sets them up properly.
    def build_request_url(self):
        self.request_url = self.gateway_url + '/' + self.service_name
        self.request_url += '/' + self.entity_set_name

        filter_string = ''

        if len(self.filters) > 0:
            filter_string = '?$filter='
            for filter in self.filters:
                filter_string += filter[0] + ' ' + filter[1]
                filter_string += ' \'' + filter[2] + '\' and '

            filter_string = filter_string.rstrip(' and ')

        if not filter_string:
            self.request_url += '?$format=json'     
        else:
           self.request_url += filter_string + '&$format=json'

    # Perform the actual request, by adding the authentication header and 
    # the filtering options to the URL. 
    def perform_request(self):
        try:
           self.build_request_url()
           if self.basic_auth:
               headers = {'Authorization':'Basic ' + self.basic_auth}
               self.result = requests.get(self.request_url, 
                                          headers=headers)
           else:
               self.result = requests.get(self.request_url)
         except Exception as e:
             raise Exception(e)

    # Utility function to return a pandas dataframe from the results of 
    # the OData request.
     def get_result_dataframe(self):
         try:
             self.perform_request()
             json_obj = json.loads(self.result.text)
             json_results = json.dumps(json_obj['d']['results'])
             return pandas.read_json(json_results).drop('__metadata',axis=1)
         except Exception as e:
             raise Exception(e)

    # Utility function to return a basic JSON object as the results of 
    # the query.
     def get_result_json(self):
         self.perform_request()
         return json.loads(self.result.text)

    # The utility function we use, to save the results to a local .csv
    def save_result_to_csv(self, file_name):
        self.get_result_dataframe().to_csv(file_name)

    # A utility to properly parse the dates that are returned in a json 
    # request.
    @staticmethod
    def odata_date_to_python(date_string):
         date_string = date_string.replace('/Date(', '').replace(')/', '')
         date_string = date_string[:-3]
         new_date = datetime.datetime.utcfromtimestamp(int(date_string))
         return new_date

定义好实用类后,我们准备执行请求。您需要用您自己的值替换斜体代码。

sales_request = GatewayRequest(gateway_url='http://YOUR_SAP_HOST/sap/opu/
 odata/sap/',
               entity_set_name='DailyMaterialSalesSet',
               service_name='ZTEST_MATERIAL_PREDICT01_SRV',
               user='YOUR_USER', password='YOUR_PASS') 

# We added three materials here, but you could add as many as you like in this
# syntax 
sales_request.add_filter('Material', 'eq', 'YOUR_MATERIAL1') 
sales_request.add_filter('Material', 'eq', 'YOUR_MATERIAL2') 
sales_request.add_filter('Material', 'eq', 'AS_MANY_AS_YOU_WANT') 

# Note for dates OData requires the below filtering syntax 
# Yes - dates are a little weird 
sales_request.add_filter('ReqDeliveryDate', 'gt',         
                         "datetime'2015-01-01T00:00:00'")

sales_request.save_result_to_csv('D:/Data/Sales.csv')

步骤 3:探索数据

现在我们已经有了数据,可以轻松将其读入 Python。我们将需要一些标准库来开始。这些是非常常见且经常使用的库:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime

我们将使用 pandas 库来读取数据:

df = pd.read_csv('D:/Data/Sales.csv')

让我们通过查看前几行来查看我们的数据:

df.head()

毫不奇怪,数据在读入 R 时遇到了与之前相同的问题。在图 6-19 中,我们看到需要删除列和调整日期列。

Python 中未转换的销售数据框

图 6-19. Python 中未转换的销售数据框

我们将在 Python 中执行与 R 中相同的功能。这些基本步骤是:

  1. 删除一列

  2. 将日期列转换为真实日期

  3. 按单一材料子集数据框

  4. 删除材料列

  5. 将日期作为数据框的索引

#Drop the column 'Unnamed'
df = df.drop(['Unnamed: 0'], axis = 1)
#Convert the date column to numeric and take out any nonnumeric chars.
df.ReqDeliveryDate = pd.to_numeric(df.ReqDeliveryDate.str.replace('[⁰-9]', ''))
#Convert the date column to a proper date using to_datetime
df['ReqDeliveryDate'] = pd.to_datetime(df['ReqDeliveryDate'], unit='ms')
#Subset the dataframe by the single material 8939
df_8939 = df['Material']==8939
df = df[df_8939]
#Drop the material column
df = df.drop(columns=['Material'])
#make the date column the index
df = df.set_index(['ReqDeliveryDate'])

让我们再次查看我们的数据框,但这次通过快速绘图(显示在图 6-20 中):

plt.plot(df)

Python 中销售初始图

图 6-20. Python 中销售初始图

然后,我们将使用 Python 中的statsmodels包执行时间序列的分解。这是一个统计任务,将时间序列对象拆解为几个类别或模式。这些模式包括观察值、趋势、季节性和残差。我们将分解我们的时间序列并绘制它(结果显示在图 6-21 中):

from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(df, model='multiplicative', freq = 52)
result.plot()

这给我们一个关于时间序列数据的总体视图。观察图表为我们提供了数据的精确表示。趋势图表显示了观察值的整体趋势。可以将其视为观察值的平滑处理。季节性图表突出显示数据是否存在任何季节性方面。如果在此处观察到重复模式,则数据可能存在季节性。最后,残差图表显示观察值和预测值之间的误差。

我们销售数据的分解图表。

图 6-21. 我们销售数据的分解图表

步骤 4:建模数据

就像我们在 R 中所做的那样,现在我们将在 Python 中创建一个 ARIMA 预测。使用pyramid.arima⁷包在 Python 中非常方便。探索该包以了解可以进行的所有设置:

from pyramid.arima import auto_arima
step_model = auto_arima(df, start_p=1, start_q=1,
                           max_p=3, max_q=3, m=12,
                           start_P=0, seasonal=True,
                           d=1, D=1, trace=True,
                           error_action='ignore',
                           suppress_warnings=True,
                           stepwise=True)
 print(step_model.aic())

日志将如打印时所运行的那样:

Fit ARIMA: order=(1, 1, 1) seasonal_order=(0, 1, 1, 12); AIC=21114.204, 
  BIC=21138.271, Fit time=2.342 seconds
. 
. 
. 
Fit ARIMA: order=(1, 1, 2) seasonal_order=(0, 1, 1, 12); AIC=21076.593, 
  BIC=21105.474, Fit time=4.335 seconds
Total fit time: 35.598 seconds

下一步是将时间序列分成两组:一组用于训练,一组用于测试。我们希望在大多数数据上进行训练,然后在剩余数据上进行测试。对于我们的数据集,时间序列从 2015-01-05 到 2018-07-21. 因此,我们决定选择从 2015-01-05 到 2018-04-01 的范围进行训练,然后用剩余的日期进行验证:

train = df.loc['2015-01-05':'2018-04-01']
test = df.loc['2018-04-02':]

下一步是将模型拟合到训练数据:

step_model.fit(train)

让我们预测在 2018-04-01 之后会发生什么,测试集中时间步数的数量。测试系列的时间步数如下所示:

len(test)
73

进行预测的命令很简单:

future = step_model.predict(n_periods=73)

要查看我们预测的结果,只需简单输入**future**。我们的future对象是从训练数据中预测的数组。我们使用了 73 个期间,因为我们希望预测的长度与我们的test数组的长度完全相同。稍后您将看到,我们可以将它们叠加在一起并可视化准确性:

array([26912.93298004, 31499.53327771, 31600.12890142, 25459.90672847,
        30282.82366396, 27135.66098529, 28756.53431911, 31096.66619926,
 … ])

步骤 5:评估模型

尽管现在看起来一切都很好,但是当我们的预测与测试数据框架的实际结果对比时会是什么样子?首先,我们需要将未来转换为具有“预测”列标题的适当数据框架。

future = pd.DataFrame(future,index = test.index,columns=['Prediction'])

接下来,我们简单地将测试和未来的数据连接起来,并绘制它们的图表,这在 Pandas 中非常容易实现(结果显示在图 6-22):

pd.concat([test,future],axis=1).plot()

ARIMA 模型的实际和预测结果。

图 6-22. ARIMA 模型的实际和预测结果

ARIMA 模型的结果看起来与该时间段的销售均值相当接*。比较预测与实际的峰值和谷值组合表明,预测的趋势与实际情况基本一致,只是程度不同。这意味着当实际数据上升时,预测往往也会在或接*该时间段上升。

查看预测与整体数据的对比非常简单(结果见图 6-23):

pd.concat([df,future],axis=1).plot()

预测结果与原始数据的对比

图 6-23. 预测结果与原始数据的对比

我们的可视化结果揭示了一个之前不明显的问题。当查看随时间变化的日销售时,我们无法真正确定从 2015 年到 2018 年销售是在下降还是在上升。这里绘制的预测清楚地显示了整体销售在下降。

总结

在本章中,我们完成了识别预测业务需求、从 SAP 中提取数据、探索数据、对数据建模和评估模型准确性的整个过程。时间序列预测是数据科学中一个迷人且多层次的领域。在我们的模型中,我们只使用了单变量时间序列数据;即,只有日期和一个数值。我们在数据中寻找可能帮助我们使用标准的 R 和 Python 中的 ARIMA 模型进行未来预测的模式。多变量时间序列分析是当有多个因素影响目标变量时。这通常更加健壮,并且可以考虑影响目标变量(如销售)随时间变化的特征。

单变量时间序列数据就是随时间变化的单一数值。想象一下股票的收盘价格随时间的变化。你可以轻松地将这里的技术应用于股票数据,并得到一些有趣的结果。然而,这种方法不够健壮,也不考虑影响股价的无数其他因素。

多变量时间序列数据是随时间变化的多个特征。让我们再次使用我们的收盘股价。我们不仅拥有股票随时间的价值,还有来自 EDGAR(见注释 2)的季度公司收益和来自 Twitter 的社交情感分析。⁸ 基于这些多个特征进行股票预测将更为稳健,但显然更为困难。作为预测的入门,我们倾向于不采用多变量分析。

回到我们的单变量时间序列数据。如果你只有销售和日期而已,像我们这里一样,你会陷入单变量分析的困境吗?也许不会。考虑一下数据中还有什么可能影响销售的因素。也许是星期几?一年中的哪一周?日期变量本身可能具有价值。在 R 和 Python 中,提取这些值是非常容易的。例如,在 Python 中,以下命令为你从日期变量中提取所有需要的帮助:

df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['week_of_year'] = df['date'].dt.week
df['day_of_week'] = df['date'].dt.weekofyear
df['day_of_year'] = df['date'].dt.dayofyear
df['day_of_month'] = df['date'].dt.day

在一个下午的工作中,数据科学家们利用 SAP 数据和常见工具进行了一些基本预测。他们向 SAP 团队的杜安展示,大富翁确实可以对销售数据进行预测。现在取决于他和业务团队决定当前的 ARIMA 模型(无论是在 R 还是 Python 中)是否足够好,或者他们是否需要更精确和稳健的模型。如果业务领导需要更高的准确性,那么很可能需要进行多变量时间序列分析,也许是递归神经网络或时间卷积网络。

¹ 时间序列:随时间索引的一系列数据点。

² 你可能不同意我们对推断和预测之间的尖锐区别及其对预测术语的使用。这是一个非常棘手的语义斜坡。然而,为了教学目的,你应该理解需要明确定义。

³ EDGAR:政府的开源电子数据收集、分析和检索系统。它使用 XBRL(可扩展业务报告语言)。相信我们,这是一个有趣的深坑。https://www.codeproject.com/Articles/1227765/Parsing-XBRL-with-Python

forecasthttps://cran.r-project.org/web/packages/forecast/forecast.pdf 由 Rob J. Hyndaman 编写,包含显示和分析单变量时间序列的方法。

https://cran.r-project.org/web/packages/tseries/tseries.pdf

⁶ 适配 ARIMA 模型有时被称为 Box-Jenkins 方法。

https://pypi.org/project/pyramid-arima/

⁸ 例如,参见https://ieeexplore.ieee.org/document/7955659https://www.tandfonline.com/doi/full/10.1080/23322039.2017.1367147

第七章:聚类与分割在 R 中

大宝贝仓库正在进行一场重大变革:他们将升级当前的 SAP 系统为 S/4HANA。此外,他们决定除非必要,否则不迁移他们的旧数据。每个部门都被要求确定其自己的关键数据。罗德作为国家客户代表,他的责任是确定系统中应该迁移哪些客户。他们有数十年的客户数据,其中许多已经过时。

罗德长期以来一直想更好地了解他的客户,因此这个过程对他来说是有益的。哪些客户是最有价值的?这项工作是否仅仅是计算客户按销售额排名前 N 名?或者是顾客购买的频率?也许是一些因素的结合。他向他的 SAP 销售与分销分析师杜安寻求建议如何解决这个问题。杜安读过这本书后立刻想到:“这是聚类和分割的任务!”

聚类是将数据集分成较小且有意义的组的几种算法之一。并没有预先确定什么维度(或多维度)最适合进行分组。从实际角度来看,你几乎总是会对你想分析的维度(或特征)有一些想法。例如,我们有销售数据,你想了解客户价值。显然,整体购买历史和金额很重要。那么顾客购买的频率呢?他们最*的购买情况呢?也许他们搬走了?在本章中,我们将使用这三个特征来演示聚类。

分割将聚类应用于业务策略。它最常见的用途是研究市场。如果你能够识别出客户(或潜在客户或机会)的群体,你就可以根据他们的集群位置确定高效的接触方式。例如,你可以基于客户在一周中哪个时段更可能对广告做出响应来对客户进行聚类,然后根据这些信息微调你的广告策略。

注意

聚类和分割经常被交替使用。然而,它们在技术上是有区别的。聚类被认为是使用机器学*将客户分组的行为。分割则用于应用聚类的结果。这看起来确实像是挑剔的语义,但它不仅仅如此。它还让我们想起一个同事,当有人错误地使用术语on-premise时发火了...冷静下来。

在本章中,我们将通过多种技术步骤来详细说明聚类和分段客户数据的过程。这是一次性报告,要交给 Rod。我们决定使用 R Markdown¹,因为这是报告结果的简便方式。不像在第五章中做的那样构建动态仪表板如 PowerBI,因为数据不会发生变化。尽管 R Markdown 可能变得非常复杂;但在我们的示例中,我们将使用基本功能。图 7-1 展示了我们将遵循的流程。我们之前见过这个流程,但这次我们添加了“报告”作为最后一步。记住,Rod 是一位国家客户代表;销售人员不希望看到代码。

用于分段客户数据的流程图

图 7-1. 用于分段客户数据的流程图

理解聚类和分段

在我们能够对客户进行分段之前,我们需要了解几种不同的聚类和分段技术。有许多不同的聚类技术,但我们将重点关注:

  • 最*性、频率和货币价值(RFM)方法

  • k-均值聚类

  • k-中心或围绕中心(PAM)聚类

  • 分层聚类

  • 时间序列聚类

RFM

RFM 是一种仅基于客户购买历史评估客户的聚类方法。情景非常直接。客户根据以下三个因素进行评估:

最*性

上次购买是什么时候?

频率

他们在给定时间段内购买了多少次?

货币价值

在这个时间段内的购买总金额是多少?

一旦这些问题得到解答,客户就会为每个因素分配一个值,通常是前 20%为 5 分,接下来的 20%为 4 分,依此类推。一旦他们在每个类别中得到了值,我们可以根据这些值对客户进行聚类。这些是个体的 RFM 。这些值被组合(连接)成 RFM 分数。例如,如果客户的最*性为 5,频率为 4,货币价值为 3,他们的 RFM 分数将是 543。

然后将这些客户放入一个类别中,可以采取行动(即在分段过程中),如表 7-1 所示。这可以是非常细粒度或更一般化的方法;在Putler 网站上有详细的选项列表。

表 7-1. RFM 客户段特征

客户段 因素 特征
冠军 R - 高 最*购买
F - 高 经常购买
M - 高 花费最多
潜在冠军 R - 高 最*购买
F - 中等 不经常
M - 高 花费很多
中等路线 R - 中至高 较*期购买
F - 中至高 有一定频率
M - Medium Spend a medium amount
Almost Inactive R - Low to Medium Haven’t bought in a while
F - Medium Had some frequency
M - Low to Medium Spent a medium to low amount
Inactive R - Low No recent activity
F - Low Not much if any frequency
M - Low Not spending much
One-Timers R - Anything Any time frame recently
F - Low Not much if any frequency
M - Anything Any monetary value
Penny Pinchers R - Anything Any time frame recently
F - Anything Any type of frequency
M - Low Low monetary value

企业对于他们认为的可能有不同的定义。对于我们的目的,我们将高定义为 4 到 5,中定义为 2 到 3,低定义为 1。

帕累托原则

许多最终用于客户分割的聚类和评估技术都基于帕累托原则,这在 图 7-2 中有所总结。

帕累托原则

图 7-2. 帕累托原则

此原则表明,Big Bonanza Warehouse 的销售的 80%来自于仅有的 20%客户。对于我们的转换任务,这意味着我们要确保这些关键客户被转化为新系统。

k-Means

k-means 是一种聚类算法,它将数值聚集在几何中心周围。使用 k-means 时,您需要定义要使用的聚类数。选择聚类数的过程既直观(您可能了解您的数据,并且知道您需要多少个聚类或组),也是实验性的。我们还将在本章后面探讨自动找到最优聚类的方法。现在,让我们选择三个聚类。该算法随机初始化三个称为质心的点。然后,它遍历每个数据点(也许是我们客户的 RFM 分数),并将它们分配给最*的质心。然后算法再次关注质心,并将它们移动到所有分配给它的点的平均距离处。这个过程重复进行,直到质心停止移动。如果质心较少,k-means 的计算速度很快。一个示例过程可能看起来像图 7-3 到图 7-6。

k-means 中的第一步 - 随机质心初始化

图 7-3. k-means 第 1 步 — 随机质心初始化

k-means 中的第二步 - 通过平均距离移动质心

图 7-4. k-means 第 2 步 — 通过平均距离移动质心

k-means 中的第三步 - 继续通过平均距离移动质心

图 7-5. k-means 第 3 步 — 继续通过平均距离移动质心

k-means 中的最终位置 - 质心平均距离收敛

图 7-6. k-means 的最终位置 - 质心平均距离收敛

k-Medoid

k-medoid 是一种数据分区算法,类似于 k-means,不同之处在于 k-means 最小化质心距离的总平方误差,而 k-medoid 最小化不相似性的总和。这使得它在处理噪声和离群值时比 k-means 更为健壮。k-m 的最常见实现是 PAM 算法。其步骤与上述 k-means 的步骤类似。

小贴士

简单地说,k-means 使用平均值,而 k-medoid 使用中位数。

分层聚类

分层聚类使用一种从下至上构建层次结构的技术。它不需要预先指定簇的数量。分层聚类有两种类型:凝聚式和分裂式。我们在 第二章 中介绍了这些概念。简要回顾一下,凝聚式聚类首先将每个点放入自己的簇中。在我们的示例中,每个客户将是一个簇。然后它标识最接*的两个簇并将它们合并成一个。它重复此过程直到每个数据点都在一个簇中。分裂式聚类则相反。我们所有的客户都放入一个簇中。该簇递归地分裂,直到所有客户都在各自的单独簇中。

有不同的连接类型可用于确定数据点配对成簇的方式。最常见的两种是完全连接和平均连接:

完全连接

找到属于两个不同簇的两点之间的最大可能距离。

平均连接

找到属于两个不同簇的点的所有可能成对距离,然后取平均值。

随着类别向上移动,这些聚类区域变得更宽更可辨,如 图 7-7 所示。分层聚类的图称为 树状图

聚类树状图。y 轴上的彩色条代表聚类的层级。

图 7-7. 聚类树状图(y 轴上的彩色条代表聚类的层级)

时间序列聚类

时间序列聚类不是我们进行客户分割的方法之一,因为它需要比常规聚类更多的计算能力和数据。然而,它是一种引人入胜的聚类类型,我们想提一下。时间序列聚类根据数据随时间变化的行为创建聚类。例如,考虑我们的情景,我们可能有一些客户在不活跃之前表现出特定的行为。他们过去的购买频率高,逐渐减少,货币价值也减少。也许我们有额外的数据,我们可以看到这些客户退货增加(因此感到沮丧)。通过对时间上的这种模式进行聚类,我们可以看到当前的客户,表现出相同的模式并属于同一聚类,尚未变得不活跃。特别是市场营销部门对了解这些客户以防止流失非常感兴趣。

提示

有一个非常强大和出色的 R 包TSclust专门用于时间序列聚类。时间序列的层次聚类不仅有用,而且非常有趣。

第 1 步:收集数据

我们已经探索了多种从 SAP 中提取数据的方式。这次我们将使用核心数据服务(CDS)视图。在第三章中,我们详细介绍了为销售数据定义 CDS 视图的过程。这正是我们将在这里使用的确切 CDS 视图,所以确保熟悉第三章!

第 2 步:清理数据

一旦我们从 SAP 下载了数据,我们就需要清理它。到目前为止,我们已经为每一章节做到了这一点,你可能还记得在介绍中提到过 SAP 的数据是干净的!这里有一些看法:如果你是一名数据科学家,你会同意这份数据非常干净。数据科学家经常处理非常不干净的数据。例如,我们最*使用的来自FDA 的橙皮书的数据有一个特别有趣的“治疗类别”栏目。这些类别可以有多个类别。如果有多个类别,它们会被添加到字段中。你永远不知道一个字段中会有多少类别,有时可能是一个类别,有时可能是十几个类别。要从这一列中获取数据,你需要拆分、堆叠、整理一些正则表达式(regex)工作,但还不够。我们用这个作为一个例子,展示一个脏列看起来会是什么样子,这并不是每天清理数据的复杂例子。从这个角度来看,SAP 的数据确实是闪闪发光的。

尽管如此,我们闪闪发光的数据仍然需要一些光泽。首先让我们加载我们接下来需要用到的库:

library(tidyverse)
library(cluster)
library(factoextra)
library(DT)
library(ggplot2)
library(car)
library(rgl)
library(httr)

R 中的 httr 库 使直接从我们的 CDS 视图中提取数据变得容易。第一步是识别 URL(请参考 “Core Data Services” 进行复*)。

url <- 'http:/<host>:<port>/sap/opu/odata/sap/ZBD_DD_SALES_CDS/ZBD_DD_SALES'

下一步很简单,只需调用服务并使用您的 SAP 凭证进行身份验证:

r <- GET(`url`, authenticate("[YOUR_USER_ID]", "[PASSWORD]", "basic"))

r 对象是一个 response 对象。它包含有关 HTTP 响应的详细信息,包括内容。使用以下命令访问内容:

customers <- content(`r`)

customers 对象是一个大列表。在我们继续之前,我们需要将其转换为更友好的格式。首先,我们将以这种方式提取列表的结果:

customers <- customers$d$results

我们仍然有一个列表,但它只是我们调用的结果列表,不包括 HTTP 的详细信息。我们可以使用 do.call 命令 将该列表转换为数据框架,并将所有行绑定在一起:

customers <- do.call(rbind, customers)
小贴士

花点时间了解 do.call。这个简单而不起眼的命令将成为您数据科学家工具箱中的一个常用工具。

最后,我们将我们的对象转换为数据框架:

customers <- as.data.frame(customers)

让我们来看看它。这一次,我们不再单独使用混乱的 head 函数,而是利用 DT。以下是我们需要运行的命令(结果显示在 图 7-8 中):

datatable(`head`(`customers`))

Datatables 视图。Datatables 是来自 DT 库的格式化良好且可排序的数据框架。

图 7-8. Datatables 视图(datatables 是来自 DT 库的格式化良好且可排序的数据框架)

正如我们已经做过几次一样,还有一些快速简单的清理工作要做 —— 摆脱一些额外的列、一些空白和一些我们不需要的列。我们还会删除 __metadata、CreateTime、CustomerMaterial、ItemCategory、DocumentType 和 DocumentCategory 列,因为它们对我们的分析没有必要:

customers$__metadata <- NULL
customers$CreateTime <- NULL
customers$Material <- trimws(tab$Material)
customers$DocumentType <- NULL
customers$CustomerMaterial <- NULL
customers$ItemCategory <- NULL
customers$DocumentCategory <- NULL

我们注意到日期以 Unix 格式传输。我们将以与我们在 第六章 中修复的方式修复它:

#Remove all nonnumeric from the date column
customers$CreateDate <- gsub("[⁰-9]", "", customers$CreateDate)

#Convert the unix time to a regular date time using the anytime library
library(anytime)

#First trim the whitespace
customers$CreateDate <- trimws(customers$CreateDate)

#Remove the final three numbers
customers$CreateDate <- gsub('.{3}$', '', customers$CreateDate)

#Convert the field to numeric
customers$CreateDate <- as.numeric(customers$CreateDate)

#Convert the unix time to a readable time
customers$CreateDate <- anydate(customers$CreateDate)

detach(package:anytime)

我们在预览数据时还注意到一些计量单位(UoM)为空白。计量单位是 SAP 中重要且复杂的主数据概念。如果我们的交易数据缺少 UoM,这表明我们不知道实际数量。例如,如果我们有 10 个数量但没有 UoM,那么我们是有 10 个每个还是 10 个箱子,每个箱子里有 12 个?如果缺少 UoM,则应排除这些条目:

customers <- customers[customers$UoM != '',]

让我们也确保字段类型适当:日期是日期,整数是整数,等等。我们还将清除出现在某些列中的空白:

#The price has commas - remove 'em
customers$NetPrice <- gsub(',', '', customers$NetPrice)

#The price should be converted to a numeric
customers$NetPrice <- as.numeric(customers$NetPrice)

#There are commas in the quantity, take them out.
customers$Quantity <- gsub(',', '', customers$Quantity)

#The quantity should also be numeric
customers$Quantity <- as.numeric(customers$Quantity)

#The date should be in a standard date format. It is currently MM/DD/YYYY.
customers$CreateDate<- as.Date(customers$Created.on, '%m/%d/%Y')

#trim the whitespace out of the unit of measure.
customers$UoM <- trimws(customers$UoM)

现在我们有了一个客户及其销售数据的数据框。让我们思考一下我们的任务。我们想要识别每位客户的特征,因此我们的数据框应该对每位客户有唯一的行。目前,我们拥有每位客户的所有销售数据。因此,我们的关键不是客户;而是订单。让我们更改这一点,并创建一个以每位客户的 RFM 值为基础的数据框,作为所有其他分析的依据。

首先,我们将处理最*性。为此,在数据框中创建一个新条目,表示自销售以来的天数:

#get the number of days since the last purchase
customers$last_purchase <- Sys.Date() - customers$CreateDate

记得那篇重要的统计论文,题为“数据分析的分割-应用-组合策略”,在第六章中引用过吗?我们将在这里使用相同的技术。我们想要一个客户的最*购买数据框。我们使用聚合函数并取最小值来实现这一点。我们正在分割主数据框以聚合它,并应用最小函数:

#create a dataframe of most recent orders by customer.
recent <- aggregate(last_purchase ~ Customer, data=customers, FUN=min, na.rm=TRUE)

以真正的组合方式将它们重新放在一起,这将创建一个按客户最*购买排序的列:

#Merge the recent back to the original
customers <- merge(customers, recent, by='Customer', all=TRUE, sort=TRUE)
names(customers)[names(customers)=="last_purchase.y"] <- "most_recent_order_days"
#What we have now is the most_recent_order_days in our original dataframe

接下来,我们将处理频率,按照与我们对最*性的处理相同的理论进行。创建一个通过按销售文档和客户聚合数据来计算订单数量的数据框。在 R 中进行计数是通过问“长度是多少?”来进行的。因为我们在单个订单上有多行,并且我们想要计算订单而不是行数,所以我们说“唯一长度是多少?”或者“订单上有多少行?”

#create a seperate dataframe of the count of orders for a customer
order_count <- aggregate(SalesDocument ~ Customer, data=customers, 
  function(x) length(unique(x)))

再次将新分割的数据框添加回原始数据,以留下分配给客户的订单计数列:

#Merge the order_count back
customers <- merge(customers, order_count, by='Customer', all=TRUE, sort=TRUE)

#Rename the field to be nice
names(customers)[names(customers)=='SalesDocument.y'] <- 'count_total_orders'

最后,我们将处理客户的货币价值。在我们的数据框中,有价格和数量的列。将它们相乘以获得每行的价值:

#calculate order values. Get per line then aggregate.
customers$order_value <- customers$Quantity * customers$NetPrice

再次以真正的分割-应用-组合方式,对每位客户的所有行进行聚合:

#Split off the aggregated value per customer.
total_value <- aggregate(order_value ~ Customer, data=customers, FUN=sum, 
                            na.rm = TRUE)

重复此过程,将新分割的数据框合并回原始数据。

`#Merge the total_value back`
customers <- merge(customers, total_value, by='Customer', all=TRUE)

`#nicify  the name`
names(customers)[names(customers)=='order_value.y'] <- 'total_purchase_value'
提示

所有这些分割出来的数据框现在已经没有用了。为了保持工作区的整洁并释放一些内存,使用这个简单的命令将它们删除:

rm(recent, order_count, total_value)

在所有这些分割和组合之后,我们需要清理一下主数据框。我们要确保每一行都是唯一的。当比较所有字段时,不应该有重复的行。毕竟,没有订单号和行号应该是多行相同的:

customers <- customers[!duplicated(customers), ]

我们还要确保没有空白的客户值。我们正在识别客户及其 RFM 值,所以显然空白的客户没有用处。不应该有空白值,但是双重检查是个好*惯:

customers <- na.omit(customers)

我们需要的是一个包含四个关键值的数据框:customer numbermost_recent_order_dayscount_total_orderstotal_purchase_value。使用 colnames 函数查看列名和位置。

colnames(customers)
 [1] "Sold.To.Pt"            "Sales.Doc..x"          "Created.on"            "Name.1"
 [5] "City"                  "Rg"                    "PostalCode"            "Material"
 [9] "Matl.Group"            "Order.Quantity"        "SU"                    "Net.Price"
[13] "Material.description"  "last_purchase.x"       "most_recent_order_days" "count_total_orders"
[17] "order_value.x"         "total_purchase_value"

我们只想要第 1、15、16 和 18 列。其他列现在不再需要,它们只是用来创建 RFM 值。我们只需按客户切片得到这些 RFM 值的数据框即可:

#slice off the required columns
customer <- customers[, c(1, 15, 16, 18)]

现在当我们查看数据框时,我们只看到了我们想要的列。但是,在这一点上我们也注意到我们有重复的行:

> head(customer)
 Sold.To.Pt most_recent_order_days count_total_orders total_purchase_value
1      1018                  153                  1            37734.08
2      1035                  138                  1               89.85
3      1082                  143                  1            36181.46
4      1082                  143                  1            36181.46
5      1082                  143                  1            36181.46
6      1082                  143                  1            36181.46

使用!duplicated命令删除重复的行:

#remove customer duplicates
customer <- customer[!duplicated(customer$Customer),]

如何才能按我们需要的方式对它们进行排名呢?这看起来像是一项艰巨的任务。不过,用 R 的话就简单了!我们只需从customer创建一个新的数据框。创建一个名为R的列,这是基于most_recent_order_days的变化值。该变化是基于 5 来创建百分比排名。也就是说,把前 20%放在 5 中,接下来的 20%放在 4 中,依此类推。对count_total_orderstotal_purchase_value重复此过程。

#Now that we have a value for each of our customers, we can create an RFM
customer_rfm <- customer %>%
  mutate(R = ntile(desc(most_recent_order_days), 5),
         F = ntile(count_total_orders, 5),
         M = ntile(total_purchase_value, 5))

现在我们有了 R、F 和 M 值,我们可以把客户列转换为索引并清理我们的工作空间:

#make the customer the row names
row.names(customer_rfm) <- customer$Customer

#ditch customer because it is an index
customer_rfm$Customer <- NULL

#clean up the workspace and free memory
rm(customer, customers)

曾经有人抱怨他们因为电脑崩溃而丢失了所有工作吗?“我已经在那上面工作了四个小时,现在全都丢了!”嗯,我们也可能遇到这种情况,所以让我们创建一个可以轻松读取的小输出,如果需要从这一点继续工作:

#We now have a clean file with customers and their RFM values.
#(recency, frequency, monetary value)
#To save time in the future, we will write this to a csv.
write.csv(customer_rfm, 'D:/Data/customer_rfm.csv')

第三步:分析数据

经过这些简单的步骤,数据就准备好分析了。我们将使用六种不同的技术进行分析。这可能有些多余,但可以说明数据分析的不同方式。这些方法包括:

  • 帕累托原理

  • k-means 聚类

  • k-medoid 聚类

  • 分层聚类

  • 手动聚类

重新审视帕累托原理

记住帕累托原理表明我们销售额的 80%由我们客户的 20%决定。我们的数据离这个原理有多接*?事实上,我们如何确定哪些客户贡献了 80%的销售额?让我们把这个概念分解成小组件:

  1. 计算销售额 80%的截止点是多少。

  2. 按最大货币值到最小货币值对数据框进行排序。

  3. 创建一个列,其值为货币价值的累计和。也就是说,它会逐行相加。

  4. 如果累计总和小于截止点,则将每个客户标记为“前 20”,如果大于截止点,则标记为“后 80”。

  5. 计算每个组中客户的百分比。

  6. 解释研究结果。

计算销售额的 80%非常简单:

#first question is what is 80% of the total sales?
p_80 <- 0.8 * sum(customer_rfm$total_purchase_value)

然后,我们将数据框按最大到最小的货币值排序:

#First step is to order the dataframe by monetary value.
customer_rfm <- customer_rfm[order(-customer_rfm$total_purchase_value),]

添加一个列到数据框中,该列是货币值的滚动总和:

customer_rfm$pareto <- cumsum(customer_rfm$total_purchase_value)

在截止点之前和之后标记客户:

customer_rfm$pareto_text <- ifelse(customer_rfm$pareto <= p_80,
                                   'Top 20', 'Bottom 80')

使用prop.table计算百分比。

prop.table(table(customer_rfm$pareto_text))*100
Bottom 80  Top 20
94.090016  5.909984

根据我们的计算,大约前 6% 的客户贡献了 80% 的销售额。这听起来与帕累托原理相差甚远,直到考虑到大宝狂购物广场有很多客户。然而,他们也有分销商和经销商。正是这些分销商和经销商驱动了绝大部分的销售。乍一看,数据中的这个特征似乎不是很有用。直到你仔细思考。大宝狂购物广场在美国各地都有商店和分销中心。分销商和经销商从分销中心而非商店获取产品。如果必须做出关闭分销中心或商店的决定,选择显而易见:关闭商店。

寻找最佳聚类

对于 k-means 和 k-medoid 聚类,我们需要手动选择最佳的聚类数。这个过程既是艺术也是科学。然而,有一些工具可以帮助我们选择最佳的聚类数。R 库 factoextra 提供了一个名为 fviz_nbclust 的方法,可以帮助找到并可视化最佳的聚类数。在我们开始 k-means 和 k-medoid 聚类之前,我们希望进行这个步骤。在这种方法中,有三个可能的选项:

肘部法

最小化聚类内平方和(wss)。wss 的总和衡量了聚类的紧凑程度。理论上,这应该尽可能小。它被称为“肘部法”,因为图表中有一个肘部,在这里增加聚类数不再对最小化 wss 有太大的贡献。

平均轮廓方法

评估每个点落入聚类的情况。较高的值表示良好的聚类效果。它通过测量聚类之间的平均距离来实现这一点。

间隙统计方法

评估总的聚类内变异。当间隙统计量被最大化时,进一步的聚类不会对值有太大的贡献。

我们将使用每一种方法,然后决定使用哪一种或哪些方法的组合。我们的数据框架有超过 25 万行数据,对于这些统计方法来说太多了。我们可以通过取足够数量的数据的代表性样本来处理这个问题,以确保分布类似。因为我们希望在采样中具有可重现性,所以我们需要设置一个种子。否则,每次运行此步骤时,随机性可能导致稍有不同的结果。我们也只希望对我们正在聚类的 RFM 值进行分析:

#Set a seed for reproducibility
set.seed(12345)
#Take only the R, F and M values from the dataframe, in columns 4,5,6
customer_rfm_sample <- customer_rfm[, c(4,5,6)]
#Take a sample using sample_n from dplyr library (in tidyverse)
customer_rfm_sample <- sample_n(customer_rfm_sample, 1000)

这些聚类算法在数据被归一化后运行效果更好。我们将对每个特征进行对数转换:

#Log transform the data
customer_rfm_sample$R <- log(customer_rfm_sample$R)
customer_rfm_sample$F <- log(customer_rfm_sample$F)
customer_rfm_sample$M <- log(customer_rfm_sample$M)

现在我们使用 fviz_nbclust 来优化和可视化我们的不同方法。首先将展示肘部法的图示(显示在 Figure 7-9 中)。

#Finding the optimal number of clusters
 fviz_nbclust(customer_rfm_sample, kmeans, method="wss")

其次是使用轮廓方法可视化最佳聚类数(结果显示在 Figure 7-10 中):

fviz_nbclust(customer_rfm_sample, kmeans, method="silhouette")

Elbow 方法的最佳聚类数。

图 7-9. Elbow 方法的最佳聚类数

Elbow 方法的最佳聚类数。

图 7-10. Silhouette 方法的最佳聚类数

最后,我们将使用间隔统计法(结果显示在图 7-11 中):

fviz_nbclust(customer_rfm_sample, kmeans, method="gap_stat")

使用间隔统计法确定的最佳聚类数。

图 7-11. 使用间隔统计法确定的最佳聚类数

每种方法都有不同的结果。Elbow 方法的图表没有明显的“拐点”。当我们看它时,看起来拐点可能在 5、6 或 7 点。轮廓方法清楚地显示出聚类之间的平均距离在 3 点处达到峰值。间隔统计法也清楚地显示出最佳聚类数为 5。我们了解我们的数据,我们对五个聚类感到满意。我们觉得三个聚类太小了。五个聚类将与我们绘制的三种方法中的两种达成一致。

K 均值聚类

一旦数据格式化并确定了聚类数,执行k-means 就很容易。第一步是设置聚类数目:

#Identify the number of clusters
 number_of_clusters <- 5

然后我们创建一个包含我们原始值的数据框(不是 RFM 值,而是来自客户的实际值):

cust <- customer_rfm[, c(1,2,3)]

因为我们的数据不服从正态分布,所以我们希望对这些值进行标准化。如果不进行标准化,我们做的图表会被压缩,不够清晰。有许多不同的方法可以用来标准化数据(先前我们使用了最小-最大缩放)。在这个例子中,我们将使用对数变换

cust$most_recent_order_days <- log(cust$most_recent_order_days)
cust$count_total_orders <- log(cust$count_total_orders)
cust$total_purchase_value <- log(cust$total_purchase_value)

现在我们简单地使用k-means方法。我们输入我们创建的数据框、聚类数以及应该运行该过程的次数。默认情况下,k-means随机初始化其起始点(或初始位置)。因此,有时它的开始可能不佳,无法很好地进行聚类。有一种简单的方法可以克服这一问题。只需多次运行k-means。每次都开始得很糟糕是极不可能的。可以使用nstart参数来指定运行次数:

#Perform the kmeans calculation on our
km <- kmeans(cust, centers = number_of_clusters, nstart = 20)

km是一个带有聚类和其他与聚类相关的属性的结构,如图 7-12 所示。对于我们的目的,我们只对聚类属性感兴趣。

大型 K 均值结构。

图 7-12. 大型 K 均值结构

我们希望创建一个包含客户详细信息和来自km的聚类的新数据框。这些聚类需要是因子:

viz <- data.frame(cust, cluster=factor(km$cluster))

现在我们准备绘制图表。我们的第一张图将是货币价值和最*性的简单ggplot图(结果显示在图 7-13 中):

ggplot(viz, aes(x=most_recent
_order_days, y=total_purchase_value, 
color=cluster)) + geom_point()

顾客数据按最*性和货币价值进行的 K 均值聚类。

图 7-13. 按最*性和货币价值对客户数据进行的 K 均值聚类

这并不是对我们群集的一个很令人满意的表示。虽然我们可以看到按颜色清晰聚集的最*性和货币价值的值,但我们看不到它们与频率的关系。如果我们尝试更改此图表,绘制最*性和频率,然后按订单值大小调整点的大小,我们将对点应用 alpha 值,因为它们太多了,这样可以让我们看到它们何时混合在一起(结果显示在 图 7-14 中):

ggplot(viz, aes(x=most_recent_order_days,
               y=count_total_orders,
               size=total_purchase_value,
               color=cluster)) +
geom_point(alpha=.05)

按最*性、频率和货币价值的客户数据 k-means 聚类

图 7-14. 客户数据的按最*性、频率和货币价值 k-means 聚类

再次,这并不是非常令人满意。当我们在二维平面上绘制超过两个变量时,结果可能令人失望。幸运的是,在 R 中有方法创建三维图表。我们将使用 carrgl 库来实现这一点(结果显示在 图 7-15 中):

#create a color scheme for our chart
 colors <- c('red', 'blue', 'orange', 'darkorchid4', 'pink1')

scatter3d(x = viz$count_total_orders,
          y = viz$total_purchase_value,
          z = viz$most_recent_order_days,
          groups = viz$cluster,
          xlab = "Log of Frequency",
          ylab = "Log of Monetary Value",
          zlab = "Log of Recency",
          surface.col = colors,
          axis.scales = FALSE,
          surface = TRUE,
          fit = "smooth",
          ellipsoid = TRUE,
          grid = TRUE,
          axis.col = c("black", "black", "black"))
注意

你可能需要将 difftime 属性的最*值更改为数值。要执行此操作,请执行以下命令:

viz$most_recent_order_days <- as.numeric(viz$most_recent
_order_days)

客户数据的三维 k-means 聚类

图 7-15. 客户数据的三维 k-means 聚类

这是对我们在 k-means 中群集的一个更令人满意的表示,并且给我们一个关于我们的客户如何分成五个群集的好主意。您可以看到图表的右上角有一组群集,代表我们最*、最频繁和最高货币价值的客户。同样,在图表的左下角,我们看到最不频繁、最不新鲜和最低货币价值的群集。

接下来,我们将使用 k-medoid 来以不同视角查看这些群集。

k-Medoid 聚类

k-medoid 与 k-means 类似,但 k-means 使用平均距离创建群集,而 k-medoid 使用中位数。这使得 k-medoid 对噪声和异常值不太敏感。正如我们前面讨论的那样,最常见的 k-medoid 聚类方法是 PAM 算法。

在我们对 k-means 的工作中,我们有一个名为 cust 的数据框,其中包含 most_recent_order_dayscount_total_orderstotal_purchase_value 的标度化(对数)值。这也是 PAM 所需的格式。pam 函数本身限制为 65,536 条观测值,因此首先需要进行抽样(在估算群集数量时我们已经完成了这一步):

cust_sample <- sample_n(`cust`, 10000)

使用以下命令执行 PAM 聚类算法:

#First identify the number of clusters
number_of_clusters <- 5
#Execute PAM with euclidean distance and stand set to
#false as we've already standardized our observations
pam <- pam(cust_sample,
           number_of_clusters,
           metric = "euclidean",
           stand = FALSE)

PAM 对象由 medoidsclustering 组件组成。要查看这些结果,请使用以下命令:

head(pam$medoids)
          most_recent_order_days count_total_orders total_purchase_value
 2126695                4.406719          0.6931472             7.799405
 10041958               4.442651          0.0000000             2.618125
 10040360               4.454347          0.0000000             4.245634
 10043047               4.330733          0.6931472             6.116488
 2911968                4.174387          1.0986123            10.480677

head(pam$clustering)
 2382503 3048698 2843476 10055962 10079604   490487
       1        2        1        1        3        4

使用 fviz_cluster 方法也很容易可视化群集(结果显示在 图 7-16 中):

	fviz_cluster(pam, geom='point',
                 show.clust.cent = TRUE,
                 ellipse = TRUE)

KMedoid PAM clustering.

图 7-16. k-medoid PAM 聚类
提示

fpc库中的pamk()pam的一个包装器。它根据最优平均轮廓宽度打印建议的聚类数量。

五个聚类的k-中心点可视化给我们带来了对我们观察结果的新视角。在这种类型的聚类中存在大量重叠,这使我们认为使用该技术的最优聚类数量比我们选择的要少。作为一项调查,我们将尝试pamk函数,该函数可以为我们确定聚类的数量:²

	library(`fpc`)
    pamk <- pamk(`cust_sample`,
            metric = "euclidean",
            stand = FALSE)

接下来我们将再次使用三维可视化来查看 pamk 认为最优的聚类数(结果显示在 图 7-17 中):

colors <- c('red',
           'blue',
           'orange',
           'darkorchid4',
           'pink1')

viz <- data.frame(cust_sample,
                  cluster=factor(pamk$pamobject$cluster))

scatter3d(x = viz$count_total_orders,
           y = viz$total_purchase_value,
           z = viz$most_recent_order_days,
           groups = viz$cluster,
           xlab = "Log of Frequency",
           ylab = "Log of Monetary Value",
           zlab = "Log of Recency",
           surface.col = colors,
           axis.scales = FALSE,
           surface = TRUE,
           fit = "smooth",
           ellipsoid = TRUE,
           grid = TRUE,
           axis.col = c("black", "black", "black"))
注意

请记住,pamk函数使用轮廓方法确定最优的聚类数量。

这些结果非常有趣,应该值得停下来思考。使用最优平均轮廓法得出两个聚类。为什么?记得我们的客户群体中有分销商和经销商。早些时候,当我们应用帕累托法则时,我们看到很少一部分客户贡献了大部分销售额。我们推测这些是我们的分销商和经销商。在前面的图表中,上方的聚类很可能代表我们的分销商和经销商。下方的聚类很可能代表普通客户。SAP 业务分析师和数据科学家应该对这些结果提出质疑。这个可视化告诉了我们什么?我们认为这个可视化告诉我们,分销商和经销商正在扭曲聚类结果。我们可能希望重新开始这个过程,但这次排除分销商和经销商。

PAMK 聚类的可视化结果。

图 7-17. PAMK 聚类的可视化结果
提示

如果你的 SAP 系统不区分分销商、经销商和普通客户,我们该如何排除分销商和经销商?我们使用pamk进行的聚类过程似乎做得相当好。保存下来自下层聚类的客户,然后重新对这个子集进行聚类过程。

接下来,我们将使用层次聚类来获得另一个视角。

层次聚类

正如我们讨论过的,层次聚类是识别观察结果中段的另一种方法。不像k-均值和k-中心点,它不要求确定聚类的数量。³

我们将执行每种类型的层次聚类中的一种。它们都有五个基本步骤:

  • 将观察结果放入数据框中,其中每列是一个用于聚类的值。

  • 缩放数据(我们将使用对数)。

  • 计算出一个不相似性矩阵(距离)。

  • 进行聚类。

  • 显示结果。

我们的第一步是创建一个新的 RFM 数据框架。我们已经做过几次这个过程了:

cust <- customer_rfm[, c(4,5,6)]

现在我们将对我们的值应用对数:

cust$R <- log(cust$R)
cust$F <- log(cust$F)
cust$M <- log(cust$M)

像我们其他的机器学*聚类技术一样,我们限制了特定数量的观察结果,因此我们必须再次对我们的数据进行采样:

cust_sample <- sample_n(`cust`, 10000)

现在我们准备创建一个不相似度矩阵。我们将使用标准默认值应用它。dist()函数返回数据矩阵行之间计算出的距离。

d <- dist(`cust`)
提示

要查看dist函数的参数和详细信息,请在控制台中输入?dist并按 Enter 键。文档将出现在 RStudio 的右侧面板中。如果这还不够,请尝试??dist以获得更多信息。

使用hclust()执行凝聚式分层聚类。或者,可以使用agnes()执行分裂式分层聚类。我们将在这里执行这两种方法:

#Agglomerative Hierarchical Clustering
hcl_a <- hclust(d)
#Divisive Hierarchical Clustering
hcl_d <- agnes(d)

使用plot命令可以轻松可视化我们的发现(凝聚式分层聚类的结果显示在图 7-18 中):

#Plot Aggplomerative HC - hang the results a bit
#to line them up.
plot(hcl_a, cex = 0.6, hang = -1)

凝聚式分层聚类图。

图 7-18. 凝聚式分层聚类图

对于分裂式分层聚类,我们也可以做同样的事情(结果显示在图 7-19 中):

plot(hcl_d, hang = -1)

分裂式分层聚类图。

图 7-19. 分裂式分层聚类图

这两个树状图显示的都是相同的内容——基于他们的最*性、频率和货币价值对客户进行聚类。由于他们的技术不同,它们会形成略微不同的聚类。

就个人而言,我们认为我们的观察(客户)作为树状图的效果不佳。首先,为了获得准确和可读的可视化效果,我们不得不大幅度地进行采样,实际上可能太多,以至于无法维护我们的观察关系的完整性。然而,这里的目的是展示另一种类型的聚类。⁴

手动 RFM

对于我们的最终技术,我们将定义手动的桶,将我们的 RFM 分数划分到其中。这是一种手动对客户进行聚类的方法。这可能看起来很简单,但它确实有效,并且满足许多类型分析的要求。

提示

有时最简单的工具是最好的。神经网络和机器学*算法的范围广泛且强大。诱人的是,我们会选择最耀眼的工具,试图让它适应我们的数据。特别是当涉及到受自然启发的算法时,我们会这样做。阅读出色的书籍Clever Algorithms,你会尝试将蚁群优化应用于一切。我们现在发表这一评论,因为在这个练*中,我们只使用“if”语句……这可能是最不起眼的技术。

手动执行 RFM 的最困难部分是定义分类。什么构成冠军客户与潜在冠军客户的区别?Big Bonanza Warehouse 向个别客户提供大量产品,但他们也有分销商。与客户相比,分销商总是看起来像冠军。他们对 RFM 模型的定义将与没有分销商的公司的定义截然不同。这个业务过程需要您与营销和销售团队密切合作,以定义 RFM 分类。为了我们的目的,我们将完全按照表 7-1 中的定义来定义它们。代码是一个简单的ifelse语句嵌套:

#What about manual clustering? Why not? Don't overlook the simple for the #shiny. 
customer_rfm$segment <- ifelse(customer_rfm$R >= 4 &
                               customer_rfm$F >= 4 &
                               customer_rfm$M >= 4,
                               'Champion', '')
customer_rfm$segment <- ifelse(customer_rfm$segment == '',
                               ifelse(customer_rfm$R >= 4 &
                                      customer_rfm$F >= 2 &
                                      customer_rfm$F <= 3 &
                                      customer_rfm$M >= 4,
                                      'Potential Champion', ''),
                               customer_rfm$segment)
customer_rfm$segment <- ifelse(customer_rfm$segment == '',
                               ifelse(customer_rfm$R >= 2 &
                                      customer_rfm$R <= 5 &
                                      customer_rfm$F >= 2 &
                                      customer_rfm$F <= 5 &
                                      customer_rfm$M >= 2 &
                                      customer_rfm$M <= 3,
                                      'Middle Of The Road', ''),
                              customer_rfm$segment)
customer_rfm$segment <- ifelse(customer_rfm$segment == '',
                              ifelse(customer_rfm$R >= 1 &
                                     customer_rfm$R <= 3 &
                                     customer_rfm$F >= 2 &
                                     customer_rfm$F <= 3 &
                                     customer_rfm$M >= 1 &
                                     customer_rfm$M <= 3,
                                     'Almost Inactive', ''),
                              customer_rfm$segment)
customer_rfm$segment <- ifelse(customer_rfm$segment == '',
                               ifelse(customer_rfm$R == 1 &
                                      customer_rfm$F == 1 &
                                      customer_rfm$M == 1,
                                      'Inactive', ''),
                               customer_rfm$segment)
customer_rfm$segment <- ifelse(customer_rfm$segment == '',
                               ifelse(customer_rfm$F == 1,
                                      'One Timers', ''),
                               customer_rfm$segment)
customer_rfm$segment <- ifelse(customer_rfm$segment == '',
                               ifelse(customer_rfm$M == 1,
                                      'Penny Pinchers', ''),
                               customer_rfm$segment)
customer_rfm$segment <- ifelse(customer_rfm$segment == '',
                               'Unclassified', customer_rfm$segment)

代码完成后,我们可以通过table语句查看结果:

table(customer_rfm$segment)
Almost Inactive           Champion           Inactive
             28933              34264               5134
Middle Of The Road          One Timers     Penny Pinchers
             61326              44640              13839
Potential Champion       Unclassified
             11907              48824

ggplot2可以快速显示我们类的视觉分布(结果显示在图 7-20 中):

ggplot(customer_rfm, aes(segment)) + geom_bar().

该图显示了一些有趣的发现。特别是,有大量客户未被分类(未分类)。大多数客户,毫不奇怪,都属于中庸类别。我们最初的目标是识别应该转换为新系统的客户。显然,冠军潜在冠军应该入选。然而,是否将中庸未分类纳入其中则由业务决定。我们的建议是谨慎行事并保留它们。然而,即使我们保留了这些大型群体,我们通过不包括几乎不活跃,不活跃,一次性购买节省一分钱大大减少了我们的转换任务。

手动分段客户的分布

图 7-20. 手动分段客户的分布

第 4 步:报告发现

我们已经完成了分析并获得了一些有趣的发现。然而,现在我们希望向其他人报告这些发现。在会议上展示代码行并不太合适。我们将使用 R Markdown 生成一个独特的报告。首先我们将编写 R Markdown 文档。然后我们将knit文档,以使其适合最终用户查看。在 R Studio 中,Knitting类似于发布。

要开始,请按照菜单路径文件 → R Markdown 在 RStudio 中开始一个新的 R Markdown 文档,如图 7-21 所示。

在 RStudio 中创建 Markdown 文档的菜单路径。

图 7-21. 在 RStudio 中创建 Markdown 文档的菜单路径

创建演示文稿的标题,并将您的名字作为作者添加(图 7-22)。

创建 R Markdown 文档

图 7-22. 创建 R Markdown 文档

让我们来看一下 R Markdown 文档的基本结构,如图 7-23 所示。

Markdown 文档的基本结构

图 7-23. Markdown 文档的基本结构

您的文档的基本信息:

  1. 您正在创建的文档类型。HTML 是默认选项,但您可以选择 PDF、Word 或 RTF 文档、GitHub 文件等。

  2. 单击 Knit 按钮创建/渲染报告。

  3. { } and 部分之间编写代码。

  4. 使用运行按钮测试特定部分的代码。

  5. 使用文本描述和记录您的代码和发现。

  6. 将图表放入文档中,并使用echo=FALSE命令隐藏运行它们的代码。

R Studio 提供了许多丰富的信息!请参考这张速查表之一:R Studio

R Markdown 代码

我们已经完成了分析,为了简洁起见,我们将使用 R Markdown 创建一个非常简单的报告作为示例。这是在 RStudio 中创建的代码:

---
title: "Customer Segmentation"
author: "Greg Foss"
date: "March 5, 2019"
output: html_document
---

```{r setup, include=FALSE}

knitr::opts_chunk$set(echo = TRUE)

knitr::opts_chunk$set(message = FALSE)

library(tidyverse)

library(ggplot2)

library(knitr)

library(kableExtra)

customer_rfm <- read.csv('D:/DataScience/Oreily/customer_rfm.csv',

stringsAsFactors = FALSE)

row.names(customer_rfm) <- customer_rfm$X

customer_rfm$X <- NULL

#RMarkedown 是显示发现的一种丰富而有益的方式。请参考

[R Markdown](https://rmarkdown.rstudio.com/index.html) 提供了丰富的信息。

Simple Customer Segmentation

Our customers are important to us. Therefore we want to know as much about them as
possible. We collected sales data from our SAP system to analyze and investigate.
In this document we will explore a small range of our customer data. If our findings
prove fruitful, we may want to continue this adventure. One of the first things we
should explain is the number of customers in our dataset.

Number of Customers


count(customer_rfm)

Customers display a recency, a frequency and a monetary value. Below is displayed
the distribution of these values for our customers and the overall average.


#使用 mutate 函数限制数量。异常值将被分组到一个值中。

在这种情况下为 100。

customer_rfm %>%

mutate(mrod = ifelse(most_recent_order_days > 100, 100, most_recent_order_days))

%>% ggplot(aes(mrod)) +

geom_histogram(binwidth = .7,

col = "black",

fill = "blue") +

ylab('Count') +

xlab('最*订单天数') +

ggtitle('最*订单的直方图')


wd <- mean(customer_rfm$most_recent_order_days)

print(paste0("平均最*订单:", wd))


customer_rfm %>%

mutate(cto = ifelse(count_total_orders > 20, 20, count_total_orders)) %>%

ggplot(aes(cto)) +

geom_histogram(binwidth = .7,

col = "black",

fill = "green") +

ylab('Count') +

xlab('订单数量或频率') +

ggtitle('订单频率的直方图')


wd <- mean(customer_rfm$count_total_orders)

print(paste0("平均订单频率:", wd))


#由于可能存在巨大的潜力,我们需要将货币价值分开。

#分销商和常规客户之间的差异

customer_rfm_big_players <-

customer_rfm[customer_rfm$total_purchase_value >= 100000 &

customer_rfm$total_purchase_value < 1000000,]

customer_rfm_big_players %>%

mutate(tpv = ifelse(total_purchase_value > 1000000,

1000000, total_purchase_value)) %>%

ggplot(aes(tpv)) +

geom_histogram(binwidth = .7,

col = "black",

fill = "orange") +

ylab('Count') +

xlab('总货币价值') +

ggtitle('客户货币价值的直方图(> 100,000)')


这只是用 R Markdown 进行报告的一个很小的例子。这只是冰山一角,希望它能激励你深入探索 R Markdown 的世界。我们将以这个简单的例子结束,因为坦率地说,我们可以单独为这个精彩的工具写一本完整的书。

## R Markdown Knit

显然,你不会用代码行来报告你的数据科学发现。R Markdown 允许你*编织*你的发现成为一份向业务分发的报告。在 RStudio 中点击 Knit 按钮,可以显示图 7-24 和图 7-25 的报告。

![渲染的 R Markdown 文档](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0724.png)

###### 图 7-24\. R Markdown 文档渲染

![渲染的 R Markdown 文档](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0725.png)

###### 图 7-25\. R Markdown 文档渲染(续)

# 摘要

我们在聚类和分割方面已经完成了相当的旅程,从概念开始,以报告形式展示给业务。请记住,我们最初的需求来自 Big Bonanza Warehouse 的 Rod。他想知道应该将哪些客户迁移到新系统,哪些客户应该留下。在数据探索中,我们对这个问题以及更多内容有了深入了解。对于新系统的迁移,我们将保留*冠军、潜在冠军、中庸*和*未分类*的客户。这将通过不转换成千上万个不必要的客户记录来减少转换时间、验证和成本。Duane 将这些发现带给主数据团队,为他们在 S/4HANA 转换中的工作做准备。

此外,结果帮助我们理解了我们客户基础中分销商的重要性,以及最*性、频率和货币价值的关键评估参数。除了 Rod 的项目,市场团队肯定也希望看到这些评估结果,以指导或验证他们的工作。

就像我们之前的所有探索一样,这只是个开始。了解你的客户是业务中一个有价值但经常被低估的方面。本章介绍的技术将帮助你深入了解你的 SAP 业务数据,并提出业务可能没有考虑到的问题。

¹ 你可以在[R Studio 网站](http://bit.ly/2lSzWLO)找到一个很棒的入门介绍和速查表。

² 如果你想知道,“为什么不简单地使用`pamk`来确定最佳的聚类数量呢?”这个过程可能计算开销很大,并且运行时间很长。

³ 在第二章中,我们讨论了层次聚类的两种类型:分裂型和凝聚型。请注意,层次聚类对异常值敏感。

⁴ 有许多方法可以显示树状图 — 搜索一下“美丽的树状图”来获取一些选项。我们喜欢的一些技术可以使用`ape#`包来实现。你可以在树状图中运用丰富的颜色和形状,创造出非常有创意的效果。这不仅仅是美学问题。可视化可以以更容易理解的方式展示数据。*《功能艺术:一种介绍》* 由阿尔贝托·开罗撰写,对于希望从他们的可视化中获得更多收获的人来说,是一本了不起的、富有洞察力的必读之作。


# 第八章:关联规则挖掘

Amir 是 Big Bonanza Warehouse 的销售副总裁。有天晚上,在亚马逊购物时,他收到了一条消息。“购买饼干的人也购买了饼干架。” “饼干架?太荒谬了。” 他想。但是他还是点开了这个项目。“饼干架只要一块钱,我试试。” 几分钟后他意识到,“我买了一件我本不打算购买的东西。我对购买和推荐感到满意。我怎样才能为我的销售和客户做到这一点?”

第二天在办公室,他叫来了 Duane,销售的 SAP 业务分析师。他解释了他的想法,并想知道他们如何做到。“我想为所有零售位置提供销售推荐。当客户购买产品时,我希望系统提供相关产品的推荐。” Duane 首先想到的是,“SAP 不做这个。”

和 Greg 以及 Paul 交谈后,Duane 得知 Amir 想要的可以通过一种称为关联规则挖掘的技术实现。我们打算从 SAP 接收销售订单并创建关联,或发现商品购买中的一般规则。我们想知道哪些产品经常一起购买。考虑杂货店:如果客户购买了面包和鸡蛋,他们购买牛奶的可能性有多大?

然而,如果你理解关联规则挖掘运用概率规则,你就会看到更多的应用:

实验室研究

基于之前的研究结果,结果的概率是多少?如果在一项研究中发生了 X 和 Y,那么发生 Z 的可能性是多少?在制药行业,恰当时机结束研究可能会带来重大的财务影响。

医疗诊断

诊断患者并不总是一个容易的过程。了解症状的共同出现有助于医疗提供者做出更准确的诊断。

班级时间表

理解学生可能选修的课程可以帮助一个组织准确利用资源,并避免排课瓶颈。

设备维护

预测制造线上的故障可以极大地提升生产力。如果一台设备经过 A、B 和 C 的维护,它出现故障的概率是多少?

客户订单帮助:作为直接升级的一个独特子集,考虑到某些产品通常因为某种原因一起购买。如果客户购买了数百块完美的方形瓷砖,他们很可能需要一些角落或奇形怪状的瓷砖来完成他们的项目。利用关联规则挖掘创建客户服务的方式,引导客户确保他们的互动第一次就能满足他们的需求。

在本章中,我们的目标是创建一个应用程序,在 SAP 中创建销售订单并为用户提供产品推荐。为此,我们使用 SAPUI5,这是一种标准的 SAP 前端技术。

现在基本的操作顺序应该很熟悉了。我们将沿用之前章节的大致操作流程,但这次我们将操作结果实施化(图 8-1)。数据科学的操作化是一个重要但常被忽视的步骤,这取决于您公司的基础设施。也许您的公司大量使用 Azure 或 Amazon Web Services。也许他们仅使用本地机器。在本章中,我们将在 R 中创建一个本地可访问的 Web 服务,但部署选项将根据您和您公司的基础设施和偏好而有所不同。

![用于在销售数据中查找关联的流程图](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0801.png)

###### 图 8-1\. 用于在销售数据中查找关联的流程图

# 理解关联规则挖掘

关联规则挖掘(ARM)中的技术全都是关于将观察结果与规则关联起来,例如我们可以将数据中的观察 X 与规则 Y 关联起来。与序列挖掘不同,¹ ARM 不关心观察结果的顺序,只关心它们一起出现的事实。ARM 是一种成熟且著名的大数据集中发现关联的方法,特别适用于分类数据。ARM 中有四个主要概念,即支持度、置信度、提升度和 Apriori 算法。

## 支持度

支持度是集合在数据中出现的频率。例如,图 8-2 显示威士忌和啤酒的购买同时发生在总购买量的 10%中。这意味着支持度为 10/100 或 10%。

*支持度(X 到 Y) → 含有 X 和 Y 的交易数 / 总交易数*

![威士忌和啤酒购买之间的关联](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0802.png)

###### 图 8-2\. 威士忌和啤酒购买之间的关联

## 置信度

置信度指示规则成立的频率。使用之前的例子,出于所有订单中,有 10 个包含威士忌和啤酒,15 个至少包含啤酒,80 个至少包含威士忌。因此,10 / (10 + 5) 是置信度值为 0.67。这是一个相当高的置信度值,表明这两种物品通常一起购买。然而,置信度可能会误导;仅仅是频繁出现的物品自然会有更高的置信度值。通过同时使用支持度、置信度和提升度等方法,可以克服这类限制。

*置信度(X 到 Y) → 含有 X 和 Y 的交易数 / 含有 X 的交易数*

## 提升度

Lift 是指在存在另一个物品的情况下某物品被购买的可能性,而不是独立购买的频率。换句话说,产品 A 如何提高产品 B 的购买可能性?使用我们的提升公式示例:lift = (10 / 10 + 5 ) / (80/100)。结果是 0.84。接* 1 的提升值表示一个物品对另一个物品没有影响。小于 1 的提升值表示可能存在替代(负提升)情况。尽管我们之前有很高的信心,但威士忌和啤酒之间没有提升的关系。事实上,提升值小于 1,表明威士忌可能是啤酒的替代品或反之亦然:

*提升(X 到 Y)→ 置信度(X 到 Y)/ ((没有 X 的 Y 的概率)/ 总交易数)*

## Apriori 算法

Apriori 算法由 R. Agrawal 和 R. Srikant 于 1994 年提出。它是在数据集中查找频繁项集的一种方法。它利用频繁项集属性的先验知识来实现这一点。在 R 的[`arules`](http://bit.ly/2ltQ3iX)库中,正是这个算法创建了关联规则。在本章后面分析数据时,我们将使用这个库。

# 运作化概述

在开始创建我们的应用程序之前,我们需要明确定义我们的编程目标。我们的过程架构并不复杂,但重要的是要理解这些过程中的组成部分,这些组成部分将实现我们的愿景。图 8-3 展示了从数据提取到在 SAP Fiori(SAPUI5)应用程序中显示的基本流程。

![程序和应用概述](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0803.png)

###### 图 8-3\. 程序和应用概述

SAP 将 SAPUI5 开发为基于 HTML5/JavaScript 的 Web 应用程序开发框架。SAP 还创建了一套名为 Fiori 的设计标准,SAPUI5 致力于帮助开发人员满足这些标准。原因是?标准的 SAP 用户界面,几十年历史的 SAP GUI,不受欢迎。

SAPUI5 帮助开发人员创建可以在从桌面大小的显示屏到移动手持屏幕上响应缩放的应用程序。采用 Fiori 设计原则的 SAPUI5 已经成为 SAP 最终用户的常见选择,包括 Big Bonanza 的销售人员。我们将详细介绍一种使用 SAPUI5 来增强销售体验并显示从我们的数据科学探险中生成的推荐内容的方法。数据科学家和 SAP 分析师请注意:就像我们在 ABAP 方面的其他简短尝试一样,本书并不旨在作为 SAPUI5 入门。

# 收集数据

此业务问题需要一个最终创建购买相关联规则的过程。这不是经常需要的事情,可能每季度进行一次。我们的计划是创建一个每季度更新一次的过程,并作为 API 到 SAP 应用程序的基础。

在 SAP 中很容易找到销售数据,在 VBAK 和 VBAP 表中。我们只想知道一起购买了哪些产品。最终,我们希望得到类似表格 8-1 的内容,其中每条记录都有单独的列来显示售出的各个项目。第一行表示一个销售了两个项目的订单,第二行表示销售了四个项目的订单,以此类推。

表格 8-1\. 展开的产品到订单映射

| item1 | item2 | item3 | item4 | item5 |
| --- | --- | --- | --- | --- |
| 产品 A | 产品 B |  |  |  |
| 产品 C | 产品 B | 产品 E | 产品 G |  |
| 产品 A | 产品 C |  |  |  |

然而,在从 SAP 表中选择数据时,我们得到的是表格 8-2。

表格 8-2\. 展开前的产品到订单映射

| 销售文档(VBAK) | 销售材料 |
| --- | --- |
| 10001 | 产品 A |
| 10001 | 产品 B |
| 10002 | 产品 C |
| 10002 | 产品 B |
| 10002 | 产品 E |
| 10002 | 产品 G |
| 10003 | 产品 Z |

这里有几件事情我们需要考虑:

+   我们不关心只有一个项目的订单;在这里没有任何关联。

+   我们希望我们的数据宽而不是长。记住,我们的目标是记录,每个记录都有列来标识订单中的各个项目,而不是每个项目在订单中单独记录。

+   我们不关心销售订单号;它只是用来分组材料。

+   转回异常检测概念,参见第五章,我们确定超过 25 行的任何订单都是异常的,简单地在 25 个项目处截断表格。

###### 注意

找到这个 25 行的截断简化了这一步骤。如果我们允许任意数量的行,我们将需要在 ABAP 中动态构建内部表,从而增加复杂性。

我们编写了一个简单的 ABAP 程序来满足我们的需求。它会读取指定日期范围内的所有销售订单项目,并创建一个符合我们要求格式的本地 CSV 文件。这将使我们的关联规则代码变得非常简单和直观。这是合并各种技术的一个很好的例子。我们可以使用 ABAP 格式化和提取 SAP 数据,然后简化 R 和 Python 的过程。通过深思熟虑地简化和设计从 SAP 提取的过程,我们将 R 过程简化为三行代码:

REPORT zgmf_sales_wide.


*Data Declarations


TABLES: vbak, vbap.

  • ty_items is our limited-to-25-items column-per-record structure.
    TYPES: BEGIN OF ty_items,
    item1 TYPE matnr,
    item2 TYPE matnr,
    item3 TYPE matnr,
    item4 TYPE matnr,
    item5 TYPE matnr,
    item6 TYPE matnr,
    item7 TYPE matnr,
    item8 TYPE matnr,
    item9 TYPE matnr,
    item10 TYPE matnr,
    item11 TYPE matnr,
    item12 TYPE matnr,
    item13 TYPE matnr,
    item14 TYPE matnr,
    item15 TYPE matnr,
    item16 TYPE matnr,
    item17 TYPE matnr,
    item18 TYPE matnr,
    item19 TYPE matnr,
    item20 TYPE matnr,
    item21 TYPE matnr,
    item22 TYPE matnr,
    item23 TYPE matnr,
    item24 TYPE matnr,
    item25 TYPE matnr,
    END OF ty_items.
    DATA: lt_items TYPE TABLE OF ty_items,
    wa_items LIKE LINE OF lt_items.
    TYPES: BEGIN OF ty_base,
    vbeln TYPE vbeln,
    matnr TYPE matnr,
    END OF ty_base.
    DATA: member TYPE ty_base,
    members TYPE STANDARD TABLE OF ty_base WITH EMPTY KEY,
    position TYPE i,
    xout TYPE string,
    iout TYPE TABLE OF string,
    l_string TYPE string,
    t_csv TYPE truxs_t_text_data,
    c_csv TYPE truxs_t_text_data,
    h_csv LIKE LINE OF t_csv.
    FIELD-SYMBOLS: <fs_str> TYPE ty_items.

Selections ₍ᐢ•ﻌ•ᐢ₎・゚。


SELECT-OPTIONS: s_auart FOR vbak-auart, "Sales Order Type
s_erdat FOR vbak-erdat, "Sales Order Create Date
s_pstyv FOR vbap-pstyv. "Sales Order Line Item Category
PARAMETERS: p_lnam TYPE char75 DEFAULT 'C:\temp'. "Directory to save to


Start-of-Selection ₍ᐢ•ﻌ•ᐢ₎・゚。


PERFORM get_data.
PERFORM write_file.


  • ROUTINES ₍ᐢ•ﻌ•ᐢ₎*・゚。

FORM get_data.

  • Select all order numbers and materials from VBAK and VBAP
  • based on the selection criteria on the first screen.
    SELECT vbak~vbeln, vbap~matnr
    INTO TABLE @DATA(lt_base)
    FROM vbak JOIN vbap ON vbak~vbeln = vbap~vbeln
    ##DB_FEATURE_MODE[TABLE_LEN_MAX1]
    WHERE vbak~auart IN @s_auart
    AND vbak~erdat IN @s_erdat
    AND vbap~pstyv IN @s_pstyv
    GROUP BY vbak~vbeln, vbap~matnr.

*Assign the work area structure to a field-symbol
ASSIGN wa_items TO <fs_str>.

*LOOP at the list of orders and materials and group this by order number
LOOP AT lt_base INTO DATA(wa) GROUP BY wa-vbeln.
CLEAR members.

*LOOP at the group (single order number) and put it into the members
*table.
LOOP AT GROUP wa INTO member.
members = VALUE #( BASE members ( member ) ).
ENDLOOP.

*How big is the members table? If it is not greater than
*one line then skip it. There is no association for one line orders.
DESCRIBE TABLE members LINES DATA(i).
IF i > 1.
CLEAR: position, <fs_str>.
LOOP AT members ASSIGNING FIELD-SYMBOL().

*We don't want to go over 25 lines on an order.
IF position = 25.
EXIT.
ENDIF.
position = position + 1.

*Create a variable for the item from item1 to item25.
DATA(item_position) = ITEM && position.

*Assign the item (let's say ITEM1) to the field-symbol.
*This is like a pointer and if it is successful we can
*move the value into our work area.
ASSIGN COMPONENT item_position OF STRUCTURE <fs_str>
TO FIELD-SYMBOL().
IF IS ASSIGNED.
= -matnr.
ENDIF.
ENDLOOP.

*Append the work area to our table of items.
APPEND <fs_str> TO lt_items.
ENDIF.
ENDLOOP.
ENDFORM.


FORM write_file.

*Create a header. This is not truly necessary, but it doesn't hurt
h_csv = 'item1' && , && 'item2' && , && 'item3' && , && 'item4' &&
, && 'item5' && , && 'item6' && , && 'item7' && , && 'item8' &&
, && 'item9' && , && 'item10' && , && 'item11' && , && 'item12' &&
, && 'item13' && , && 'item14' && , && 'item15' && , && 'item16' &&
, && 'item17' && , && 'item18' && , && 'item19' && , && 'item20' &&
, && 'item21' && , && 'item22' && , && 'item23' && , && 'item24' &&
, && 'item25'.

*Loop at the table of items and write it to a work area separated by commas
LOOP AT lt_items INTO DATA(items).
CLEAR xout.
DO.
ASSIGN COMPONENT sy-index OF STRUCTURE items TO FIELD-SYMBOL().
IF sy-subrc <> 0.
EXIT.
ENDIF.
IF sy-index = 1.
xout = .
ELSE.
l_string = .
xout = xout && , && l_string.
ENDIF.
ENDDO.
APPEND xout TO iout.
ENDLOOP.

*First append our header to the final csv output table
*then append all the lines of the csv.
APPEND h_csv TO t_csv.
APPEND LINES OF iout TO t_csv.

*Use SAPs standard download method to create a file and download it locally
CALL METHOD cl_gui_frontend_services=>gui_download
EXPORTING
filename = p_lnam && sales_wide_ &&
sy-datum && sy-uzeit+0(4) && '.csv '
CHANGING
data_tab = t_csv.
ENDFORM.


# 清洗数据

我们总是需要对从 SAP 获取的数据进行一些清理。但是,由于我们编写了自己的小型定制程序来提取数据,我们注意确保数据是干净的。重要的是不要对提取程序的效果作任何假设,因此我们将读取 CSV 文件到 R Studio 并查看它(结果显示在图 8-4 中):

investigate <- read.csv("D:/DataScience/Data/mat.csv")
library(DT)
datatable(head(investigate))


![调查 SAP 销售数据的宽格式数据。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0804.png)

###### 图 8-4\. 调查 SAP 销售数据的宽格式数据

事情看起来正如我们所希望的,只是 X 列是个例外。然而,这是我们的`read.csv`函数添加的内容。我们可以使用`row.names = NULL`参数来避免这个问题。当我们在下一步中以不同的方式加载数据时,我们将不会遇到这个问题。

# 分析数据

使用[`arules`包](http://bit.ly/2ltQ3iX)可以非常轻松地获得一些惊人的结果。因为我们已经使用 ABAP 格式化了我们的数据,所以可以使用以下代码将其转换为 R 中的[`transaction`对象](http://bit.ly/2lvlPMm):

transactions <- read.transactions("D:/DataScience/Data/mat.csv",
format = "basket",
sep = ',',
rm.duplicates=TRUE)


要基于加载的交易创建规则,请使用以下代码。这是先前提到的 Apriori 算法发挥作用的地方。我们将支持设置为最低 0.1%,置信度设置为 80%。由于数据集庞大且多样化,支持较低。我们有超过 50 万行的项目集。0.1%的支持仍然是 500 次出现。置信度为 0.8 意味着规则被认为是真实的 80%的时间。

###### 注意

数据科学是业务逻辑、艺术和实际机器学*知识的结合。适当设置支持和置信度值需要一定程度的试验和错误。

rules_transactions <- apriori(transactions,
parameter = list(supp = 0.001, conf = 0.8))
rules_transactions <- sort(rules_transactions,
by="confidence",
decreasing=TRUE)


我们可以使用以下命令查看我们的结果与置信度、提升和支持。

inspect(head(rules_transactions))

 lhs          rhs     support     confidence lift     count

[1] {4614440} => {79353} 0.001040583 1 2.426768 2
[2] {4360037} => {79353} 0.001040583 1 2.426768 2
[3] {8996481} => {79353} 0.001040583 1 2.426768 2
[4] {8709402} => {79353} 0.001040583 1 2.426768 2
[5] {8135285} => {79353} 0.001040583 1 2.426768 2
[6] {2911738} => {79353} 0.001040583 1 2.426768 2


Lhs 代表*左侧*;rhs 代表*右侧*。右侧的项目经常与左侧的项目一起购买,列出的支持、置信度和提升。虽然我们的支持值不是很高,但我们拥有的数据量足以提供顶级值之间的良好置信度和提升。例如,上面的第 1 行表明当购买项目 4614440 时,有 100%的置信度会购买项目 79353。此外,这种关系的提升为 2.4267。(请记住,接* 1 的提升值表示一个项目对另一个项目没有影响。)

我们已经创建了我们的关联规则;现在我们将它们保存为*transaction*对象,以便稍后在我们的操作化中使用:

save(rules_transactions,
file = "D:/DataScience/Oreily/association_rules.RData")


###### 注意

我们首先将在本地层面进行操作化,然后再向更普遍的层面发展。

在我们操作化之前,我们想要测试如果分析一个简单的结果会发生什么。从一个数据框中创建一个简单的向量,其中包含顶部的结果:

dataset <- as.vector(t(c("8135285")))


现在将创建的规则与我们向量的结果进行匹配:

matchRules <- subset(rules_transactions, lhs %ain% dataset)


像我们之前用`inspect`函数一样检查这些规则。我们看到它返回了我们之前手动检查规则时得到的相同值:

inspect(matchRules)
lhs rhs support confidence lift count
[1] {8135285} => {79353} 0.001040583 1 2.426768 2


现在要创建一个简单的 API,我们需要首先从以下代码创建一个函数,其中*数据集*作为输入变量:

subset(rules_transactions, lhs %ain% <input_vector>)


您可以使用[`plumber`库](https://www.rplumber.io/)在 R Studio 中快速创建简单的 Web API。要使用我们将在此概述的某些功能,您需要在 R Studio 版本 > 1.2 上进行操作。创建 Web API 的第一步是使用菜单路径 文件 → 新建文件 → 管道 API 打开一个新的 plumber 文件,如图 8-5 所示。

![创建您的第一个管道 API。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0805.png)

###### 图 8-5\. 创建您的第一个管道 API

这将为`plumber`提供一个基本文件,并包含一些示例。我们将放弃这些示例,使用以下代码:

library(plumber)

Load the association rules created in the load program

load(file = "D:/DataScience/Oreily/association_rules.RData")

* Send back the confidence and lift

* @param input Material Number

* @get /arm

function(input) {

Convert the input value(s) into a vector.

dset <- as.vector(t(c(input)))

Create a subset of rules matching the input

match_rules <- subset(rules_transactions, lhs %ain% dset)

Display/Return those values (by default JSON)

inspect(match_rules)
}


这段代码的人类语言意思是,“接收输入并将其转换为向量,以便我们可以进行搜索。创建一个新对象,它是与 lhs(左手边)匹配的关联规则的子集。使用`inspect()`函数返回该结果。”

当从浏览器查询时,上述代码将以 JSON 格式呈现关联规则结果。在 R Studio 中使用 Plumber API 非常简单;只需点击窗口右上角的“运行 API”按钮即可。

将出现一个窗口,允许对 API 进行审查和测试。显示在图 8-6 中。

![Swagger 和管道 API。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0806.png)

###### 图 8-6\. Swagger 和管道 API

点击“试一试”按钮并输入物料号。完成后,点击执行按钮,如图 8-7 所示。

![使用物料号测试管道 API。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0807.png)

###### 图 8-7\. 使用物料号测试管道 API

Web API 的结果显示在响应部分中,如图 8-8 所示。

![管道 API 调用的结果。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0808.png)

###### 图 8-8\. 管道 API 调用的结果

API 的结果以 JSON 格式显示如下:

+   LHS (前提)

+   RHS (结果)

+   支持

+   置信度

+   Lift

这些数据都可以用于提供销售推荐的应用程序。我们成功创建了一个 Web API,但仅限于我们的本地计算机。有许多方式来托管和发布 API。这主要受您公司环境的管理。您的公司使用 Azure、Amazon Web Services、Digital Ocean 还是其他什么?也许根本没有云环境,而是部署在本地服务器上。选择太多,无法在这本小书中详细介绍。²

###### 注意

记住:我们这里不是在构建一个完整的移动应用程序。此场景假设 Big Bonanza 公司已有一个基于 SAPUI5 的 Fiori 应用程序,并且 Greg、Paul 和 Duane 只是添加了一些额外的逻辑。这里提出的所有更改都是虚构的例子,虽然需要了解 HTML、JavaScript 和 XML,但并不需要在其本地编程语言中开发完整功能的 iOS 或 Android 应用程序。

## Fiori

我们有一个操作性的、可通过网络访问的参考点,可以使用我们的 Plumber API。正如本章开头讨论的那样,Big Bonanza 使用基于 SAPUI5 的 Fiori³应用程序,允许现场销售人员通过智能手机输入销售订单。在深入进行有趣的数据科学场景之前,SAP 团队的 Duane 在设计销售订单录入应用程序时做了一些工作。他在移动设备上简化了通常在桌面 SAP GUI 中可能非常复杂的操作到几个屏幕上。

为了让现场销售人员真正推动那些小额额外销售,让我们为 Duane 的订单录入应用程序制定一个小增强计划。我们将添加一个屏幕,在销售人员确认新订单后弹出,列出通常与订单商品一起购买的附加材料。销售人员可以根据需要向客户推荐这些项目中的一个或多个,以便立即将其添加到订单中。

###### 注意

访问[*https://open.sap.com/*](https://open.sap.com/) 并搜索“SAPUI5”,了解更多关于为 Fiori 体验构建 SAPUI5 应用程序的信息。

SAPUI5 应用程序遵循一种常见的模型-视图-控制器⁴结构。“视图”文件定义了屏幕上元素的布局。“控制器”文件定义了行为和逻辑。在这两者中,都有关于“模型”的引用,它定义了如何在客户端设备上存储数据以供应用使用。对于我们的用例,我们将修改视图文件以创建一个小弹出屏幕,用于显示建议的新产品。我们将创建一个新的模型来保存关于建议产品的信息。最后,我们将修改控制器文件,确保弹出屏幕在正确的时间出现。

Big Bonanza 有一个非常简化的 UI,类似于图 8-9。在选择客户后,只需在最后一个屏幕上添加项目即可提交到 SAP 以创建订单。

我们将把我们的推荐流程放入销售人员点击“完成订单”的流程中。让我们从管理我们按钮的视图文件开始。

在主视图文件(*Table.view.xml*,管理此屏幕)中,SAPUI5 开发人员已经在页脚中定义了按钮。我们可以快速检查一下,看看我们可以在哪里连接我们的额外逻辑:

<footer>
<OverflowToolbar>
<ToolbarSpacer/>
<Button text="Add Product"/>
<Button text="Complete Order" press="onOrderPress" type="Accept"/>
</OverflowToolbar>
</footer>


![简化订单录入,能够添加其他产品或完成订单。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0809.png)

###### 图 8-9\. 简化订单录入,能够添加其他产品或完成订单

“完成订单”按钮的`press`属性告诉我们,当用户点击该按钮时将执行什么功能(`onOrderPress`)。所以让我们进入那段代码,在*Table.controller.js*文件中:

// SNIP! Lots of other application controller code
onOrderPress: function (oEvent) {
// If the dialog box has never been opened, we initiate it
if (!this._oDialog) {
var oSuggestionsModel = new JSONModel();
this.getView().setModel(oSuggestionsModel, "suggestions");
this._oDialog = sap.ui.xmlfragment("Table.RecommendDialog", this);
this._oDialog.setModel(oSuggestionsModel, "suggestions");
}

 // Retrieve the product already entered on the screen,
 // build a query URL to the Big Bonanza ARM endpoint, 
 // then load that data into an intermediate placeholder, ARMModel. 
 var oModel = this.getView().getModel();
 `var` product = oModel.getProperty("/ProductCollection/0/ProductId");
 `var` bigBonanzaInternalUrl = "[FILL_IN_YOURS]";
 `var` oARMModel = `new` JSONModel();
 `var` endpoint = bigBonanzaInternalUrl + "/arm?input=" + product;
 oARMModel.loadData(endpoint, {}, `false`);

 // Based on results from the ARM retrieval, create a filter to retrieve
 // the full product information for the recommended products.
 `var` armData = oARMModel.getData();
 `var` aFilters = [];

 // The "Filter" object sets up the OData filter for SAPUI5
 `for` (`var` i = 0; i < armData.length; i++) {
     aFilters.push(`new` Filter("ProductId", "EQ", armData[i].rhs);
     `var` finalFilters = `new` Filter({
         filters: aFilters,
         and: `false`
     });
 }

 // The base OData model is the OData API that is serving out the rest
 // of the data points of this app. This is the API that houses the
 // "ProductCollection" endpoint, where we can retrieve more details
 // about the recommended data.
 `var` baseODataModel = `this`.getView().getModel();
 `var` that = `this`;
 baseODataModel.read("/ProductCollection", {
     filters: finalFilters,
     success: `function` (oData) {
         // In here, we assign the suggestions to that model and open
         // the dialog box. See the "Table.RecommendDialog" listing.
         `var` oSuggestionsModel = that.getView().getModel("suggestions");
         oSuggestionsModel.setData(oData.results);
         that._oDialog.open();
     }
 });

}
// SNIP! Lots of other application controller code.


这个拼图还有一个碎片。在`onOrderPress`函数的顶部附*,我们调用了一个 XML 片段。该片段定义了按下“完成订单”后出现的弹出对话框的外观和感觉(图 8-10)。

![在手机上,建议列表显示在现有订单项目列表的顶部,并允许选择一个或多个项目添加到销售订单](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0810.png)

###### 图 8-10\. 在手机上,建议列表显示在现有订单项目列表的顶部,并允许选择一个或多个项目添加到销售订单。

要设置建议列表对话框,请在您的 SAPUI5 项目中创建名为*RecommendDialog.fragment.xml*的文件,并添加以下 XML 内容:

<core``:``FragmentDefinition
xmlns="sap.m"
xmlns:core="sap.ui.core">
<SelectDialog
noDataText="No Products Found"
title="Suggested Add-Ons"
confirm="handleClose"
cancel="handleClose"
multiSelect="true"
items="{
path: 'suggestions>/'
}" >
<StandardListItem
title="{suggestions>Description}"
description="{suggestions>ProductId}"
type="Active" />
</SelectDialog>
</core``:``FragmentDefinition>


# 摘要

通过中间过程提示销售相关项目,Duane 为销售副总裁 Amir 提供了一个强大的工具,以促使其销售团队立即进行增值销售。我们使用了从 SAP 表中收集的销售数据,在一个 ABAP 程序中将其整理成一个 CSV 文件。使用 R,我们分析了这些数据,寻找了关联规则挖掘中的三个关键因素:支持度、置信度和提升度。

`arules`包为我们提供了快速分析这三个因素原始数据的方法。我们在其上层添加了一个函数,以便快速输入产品编号并输出具有强关联的 1 到*n*个产品。使用 R 中的`plumber`库,我们快速将该函数转换为可通过 Web 调用的 API。

考虑到销售团队成员在现场使用 SAP Fiori 应用程序在其手机上为客户录入销售订单,我们研究了如何快速调整 Fiori 应用程序的 SAPUI5 代码库,向用户展示“建议项目”提示。在提交订单之前,这为他们提供了一个最后的增值销售工具。并非每个客户都选择添加增值项目,但足够多的客户选择这样做,这对 Amir 的销售数字产生了积极的影响。

关联规则挖掘长期以来一直存在。将其交给 SAP 用户使用是成熟方法的新尝试;信息就在那里,可以随时获取!

¹ 序列挖掘是一种关联规则挖掘的一种类型,但此处未包括。有些事情就是无法被实现。

² 我们将在博客中提供有关使用 Digital Ocean 作为公共 API 平台的后续文章,并结合 R 和 plumber 进行介绍。

³ 还要记住,SAPUI5 是一个 Web 应用程序开发框架。在过去四到五年中,SAP 的用户体验能力有了很大的发展,而 SAPUI5 正是这些变化的领导者。

⁴ [模型-视图-控制器结构](https://zh.wikipedia.org/wiki/Model–view–controller)是软件开发中最古老、最常用的架构之一,特别适用于图形用户界面。


# 第九章:使用 Google Cloud 自然语言 API 进行自然语言处理

> “消费者因为糟糕的服务经常会放弃公司吗?一直是。”
> 
> 哈佛商业评论,[“停止试图取悦您的客户”](http://bit.ly/2k4jYhh)

杰娜是大宝石仓储的客户服务高级总监。根据 CEO 的说法,这份工作很简单:“将愤怒的客户转变为满意的客户。”愤怒的客户有很大的力量来伤害公司,因为他们不仅可以停止交易,还可以通过社交媒体发表投诉来扩大其影响。大宝石(以及全世界的其他公司!)对客户在线上发表的言论非常敏感。

客户通过消费者面向的网上商店前端注册他们的投诉,使用设计用来收集问题描述的联系表单。大宝石直接将此联系表单连接到 SAP 客户关系管理(CRM)以捕获这些注释并创建可跟踪的投诉文档。CRM 投诉创建后,杰娜的团队介入。她的团队每天处理数百个投诉。他们尽最大努力快速反应并提供高质量的服务,但杰娜知道在每天的投诉堆中有些客户如果不能快速获得高质量的服务就会流失。

SAP 业务分析师杜安也对 CRM 有深入的了解。杰娜向他提出了一个有趣的想法:“我有预算可以给那些投诉的客户小礼物或者优惠。这不是一个很大的预算,所以我必须小心如何分配。我希望尽快识别出最不满意的客户,这样我可以把这个预算应用到他们身上。”她的假设是,在流程早期采取行动可以防止客户流失和不良的社交媒体消息,但要验证这个假设,她需要快速找到潜在的流失客户。

杜安从与格雷格和保罗的交谈中得知,数据科学在自然语言处理(NLP)领域不断取得显著进展。他问道:“如果我能从 CRM 中提取这些文本投诉,我们是否可以使用 NLP 快速识别可能流失的客户?”格雷格和保罗向杜安展示了利用公共可用云 API 有效地检查投诉情感的可能性,从而避免了其他大宝石 SAP 项目中需要使用建模和训练的过程。

在本章中,我们的目标是建立一个快速的时间分析流水线,从客户投诉中提取情感,以帮助杜安推荐特别支持珍娜团队关注的最佳候选人。为此,我们将使用 ABAP 从 SAP CRM 中提取客户投诉注释,然后利用 Google Cloud API 来发现每个投诉中的正面或负面人类情感。

###### 注意

这可能是本书所有章节中最实用的部分。这是有意为之的!我们希望你明白,有时候建模已经完成,你可以使用预先存在的工具专注于业务场景。其中一个最好的方法是使用来自亚马逊网络服务、微软 Azure、Google Cloud 等提供的云 AI 工具包。为了专注于场景,我们不得不选择一个工具包,所以我们选择了 Google。这并不是对其他工具的不敬。

# 理解自然语言处理

自然语言处理是人工智能的一个子领域,专注于使计算机理解人类语言。自然语言处理的两个最常见的领域是情感分析和翻译。

## 情感分析

常被称为意见挖掘,情感分析试图从文本中识别出意见。有两种基本的情感分析方法:基于规则的方法和基于机器学*的方法。有时还会有两者的混合方法。基于规则的方法使用一组手工制作的规则。想象一个包含诸如 *awesome, incredible, neat,* 和 *good* 等正面词汇的列表标记为 **正面情感**。另一个标记为 **负面情感** 的列表则包含诸如 *terrible, awful, sad,* 和 *depressing* 等词汇。简而言之,基于规则的方法计算这些词在句子、段落或整本书中的出现次数,并决定整体情感是 **正面** 还是 **负面**。机器学*方法更为现代化和复杂。在 R 和 Python 中都有成熟的库,如 [`topicmodels`](http://bit.ly/2m1rgTz) 和 [`NLTK`](https://www.nltk.org/),使情感分析更简单。

这两种方法都使用了去除停用词、分词、词干提取和词形还原等技术,将人类语言转换成更易于分析的形式。

去除停用词就是简单地消除在意见或情感中几乎没有价值的单词,比如 *and, but, or,* 和 *the*。

分词是将一个单词序列分解成片段或标记的过程。在此过程中通常会丢弃诸如标点符号之类的烦人东西。图 9-1 是一个已经去除停用词并进行了分词的句子的示例。

![去除停用词和对句子进行分词。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0901.png)

###### 图 9-1\. 去除停用词和对句子进行分词

词干提取和词形还原是去除词形变化的过程。词干提取简单地去除词尾以获得单词的“词干”,而词形还原则使用更复杂的方法找到单词的真正基础形式。这些是简单的过程,但没有示例很难解释清楚。图 9-2 展示了如何根据标准规则对一组词进行词干提取,而 图 9-3 展示了如何对一组词进行词形还原。¹

![根据波特算法的词干提取规则](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0902.png)

###### 图 9-2\. 根据波特算法的词干提取规则

![词形还原。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0903.png)

###### 图 9-3\. 词形还原

这是情感分析的基础。然而,在本章中,我们不会构建自己的情感分析器。相反,我们将使用公开可用的谷歌云 API 来为我们进行分析。

## 翻译

在 *银河系漫游指南* 中,道格拉斯·亚当斯向读者展示了“宇宙中最奇特的事物”:一只巴别鱼。这个小生物以脑电波为食,放入耳中后能翻译任何语言。有了机器学*翻译,这样的事情是可以预见的。现代机器学*翻译使用神经网络学*将一种语言翻译成另一种语言。(我们在 第二章 中介绍了神经网络。)机器学*翻译的准确性和可靠性已经达到(有人认为已经达到)人类水平的翻译能力。

# 准备云 API

有了对自然语言处理的基本理解,让我们开始帮助杜安和吉娜得到他们所需的东西。我们将通过谷歌云来运行这个例子,但读者应该注意,亚马逊 Web 服务 (AWS) 和微软 Azure 也提供类似的服务。

在一个充满公开可用人工智能服务的世界中,我们   在一个充满公开可用人工智能服务的世界中,我们喜欢谷歌建立他们的工具集的方式。他们的库质量高,简单且灵活。更重要的是,他们数十年的机器学*研究*在你的指尖。使用谷歌云 API 几乎可以说是轻而易举的事情。让我们通过设置谷歌云平台来处理这个场景开始吧。

要使用谷歌的云服务,你需要一个谷歌账号。如果你有 Gmail 账号,或者你的公司使用 Google for Work 或 GSuite,你已经准备就绪了。否则,请前往 [*https://accounts.google.com/signup*](https://accounts.google.com/signup) 注册一个账号。这与注册 Gmail 账号是一样的;在过程结束时你将拥有一个新的电子邮件地址。

有了那个 Google 账号,你已经准备好访问这些 API 了。前往 [*https://console.cloud.google.com/*](https://console.cloud.google.com/) 开始设置正确的访问权限。根据你的账户类型,可能需要设置一个计费账户。别担心:在本章的示例中,免费的服务层将足以让我们进行学*和进展。只有当你开始为成千上万个 API 请求使用这些服务时,才会开始计费,那时我们希望你能为你的业务提供如此多的价值,以至于这不再重要。

在 Google Cloud Platform 控制台中,你需要给自己授予访问我们将用于 Jeana 工作的 API 的权限。从 APIs & Services 部分开始,如 Figure 9-4 中所示。

![点击“APIs & Services”,然后点击“Dashboard”以控制 AI APIs。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0904.png)

###### Figure 9-4\. 点击“APIs & Services”,然后点击 Dashboard 以控制 AI APIs

你将看到一个类似 Figure 9-5 的屏幕。只需点击屏幕顶部的“启用 API 和服务”。

![点击“启用 API 和服务”以进入搜索仪表板。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0905.png)

###### Figure 9-5\. 点击“启用 API 和服务”以进入搜索仪表板

你随后将进入主屏幕,可以搜索谷歌的数百个 API。对于 Jeana 的情况,我们将仅启用自然语言 API。在搜索栏中搜索“language”开始。你应该看到一个类似 Figure 9-6 的结果。点击“Natural Language API” 的结果。

![在 API 控制台搜索中搜索“language”。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0906.png)

###### Figure 9-6\. 在 API 控制台搜索中搜索“language”

点击详细屏幕中的启用按钮,启用 Cloud 自然语言 API,如 Figure 9-7 中所示。

![启用 Cloud 自然语言 API。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0907.png)

###### Figure 9-7\. 启用 Cloud 自然语言 API

一旦你点击了启用按钮,API 的开启就完成了。但在我们的准备工作中还有一个步骤。我们需要获取一个具有使用我们刚刚启用的 API 所需凭证的服务账号。从主 Google Cloud 控制台屏幕 ([*https://console.cloud.google.com*](https://console.cloud.google.com)),点击“IAM & Admin”,然后点击“Service accounts” 如 Figure 9-8 中所示。

![服务账号导航。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0908.png)

###### Figure 9-8\. 服务账号导航

在服务账户屏幕上,点击顶部的“创建服务账户”以进入服务账户创建向导。在向导的第一个屏幕上,填写类似于 图 9-9 的详细信息。确保编写一个良好的描述,以便在两年后返回此项目时,您可以记忆起每个用户执行何种角色的文档(相信我们,您可能需要重新了解!)。

![服务账户详情。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0909.png)

###### 图 9-9\. 服务账户详情

在底部点击“创建”并进入第二步。您希望该服务账户拥有我们正在处理的项目的完整所有权,因此请按照 图 9-10 中所示授予账户完整的项目所有权。

最后,在向导的最后一页上创建一个 JSON 私钥 图 9-11。这是一个文件,将允许您的计算机直接从 Python 命令行进行服务请求,使用与刚刚创建的服务用户相同的凭据。确保将该文件保存在您将创建 Python 脚本的地方。

![授予服务账户完整的项目所有者权限。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0910.png)

###### 图 9-10\. 授予服务账户完整的项目所有者权限

![创建一个 JSON 私钥文件。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0911.png)

###### 图 9-11\. 创建一个 JSON 私钥文件

在一个快速的命令中,让我们设置好并准备使用 Google Python 客户端。在您喜爱的 Python 环境中,运行以下 pip 命令:

$ pip install --upgrade google-cloud-language


就这样!您已经设置好并准备使用 Google Cloud APIs 来解决像 Jeana 在 Big Bonanza 面对的问题一样的问题。

# 收集数据

就像在 第八章 中一样,最简单的路径是以 CSV 格式输出带有相关客户备注的投诉文档。我们创建了此处显示的 ABAP 程序以满足此需求:

REPORT zcomplaint_csv.

"We kept the variable declarations at the top to
"increase readability of comments
"with code below.
DATA csv_line TYPE string.
DATA csv_table TYPE TABLE OF string.
DATA full_note TYPE string.
DATA core TYPE REF TO cl_crm_bol_core.
DATA header TYPE crmst_adminh_btil.
DATA entity_up TYPE REF TO cl_crm_bol_entity.
DATA query TYPE REF TO cl_crm_bol_dquery_service.
DATA valid_from TYPE string.
DATA valid_to TYPE string.
DATA result TYPE REF TO if_bol_entity_col.
DATA entity TYPE REF TO cl_crm_bol_entity.
DATA entity_header TYPE REF TO cl_crm_bol_entity.
DATA entity_textid TYPE REF TO cl_crm_bol_entity.
DATA textid_col TYPE REF TO if_bol_entity_col.
DATA bt_textid TYPE crmst_textid_btil.

"PARAMETERS sets up the SAP GUI screen to accept input,
PARAMETERS: p_from TYPE dats,
p_to TYPE dats.

"CRM uses the Business Object Library to provide
"query services to access its data.
core = cl_crm_bol_core=>get_instance( ).
core->load_component_set( 'BTBP' ).

"Use t-code GENIL_BOL_BROWSER to find the right query,
query = cl_crm_bol_dquery_service=>get_instance(
iv_query_name = 'BTQCompl' ).

"Here and in the next block we limit the query to the two
"dates entered on the input screen.
valid_from = p_from.
query->add_selection_param(
EXPORTING
iv_attr_name = 'VALID_FROM'
iv_sign = 'I'
iv_option = 'EQ'
iv_low = valid_from ).

valid_to = p_to.
query->add_selection_param(
EXPORTING
iv_attr_name = 'VALID_TO'
iv_sign = 'I'
iv_option = 'EQ'
iv_low = valid_to ).

"get_query_result( ) invokes the query.
result = query->get_query_result( ).

"This WHILE loop moves through each query result
"one at a time.
entity ?= result->get_first( ).
WHILE entity IS BOUND.
"To get the text data from the complaint, we have to
"move through several BOL relations. Again see GENIL_BOL_BROWSER.
entity_up = entity->get_related_entity(
iv_relation_name = 'BTADVSCompl' ).
entity_header = entity_up->get_related_entity(
iv_relation_name = 'BTOrderHeader' ).
entity_textid = entity_header->get_related_entity(
iv_relation_name = 'BTHeaderTextIdSet' ).
textid_col = entity_textid->get_related_entities(
iv_relation_name = 'BTTextIdHAll' ).

 "Retrieve header information to get the object ID - the 
 "number of the complaint document. 
 entity_header->if_bol_bo_property_access~get_properties( 
   IMPORTING 
     es_attributes = header ). 

 csv_line = header-object_id && ','. 

 "This WHILE block goes line by line through the text 
 "lines in the complaint to build one long string of text. 
 CLEAR full_note. 
 entity_textid ?= textid_col->get_first( ). 
 WHILE entity_textid IS BOUND. 
   entity_textid->if_bol_bo_property_access~get_properties( 
     IMPORTING 
       es_attributes = bt_textid ). 

   IF bt_textid-conc_lines IS NOT INITIAL. 
     CONCATENATE full_note bt_textid-conc_lines 
       INTO full_note RESPECTING BLANKS. 
   ENDIF. 

   entity_textid ?= textid_col->get_next( ). 
 ENDWHILE. 

 "Safety check - if there were no actual texts added 
 "don't send this to the .csv 
 IF full_note IS NOT INITIAL. 
   csv_line = csv_line && full_note. 
   APPEND csv_line TO csv_table. 
 ENDIF. 

entity ?= result->get_next( ). 

ENDWHILE.

"Document downloads to end user's computer.
cl_gui_frontend_services=>gui_download(
EXPORTING
filename = 'C:\Users\paul\Desktop' &&
sy-datum && sy-uzeit+0(4) && '_Complaints.csv'
CHANGING
data_tab = csv_table ).


###### 注意

这个 ABAP 程序在本书中与其他 ABAP 写作的程序不同,它不直接使用 SELECT 语句检索数据。SAP CRM 使用关系表,就像其他章节中的 ECC 系统一样,但它设计为通过 SAP 的业务对象层(BOL)技术访问。BOL 定义了业务对象之间的关系(例如,服务订单与其消耗的零件之间的关系),并允许程序员使用这些关系,而无需了解底层的表结构。

在 SAP GUI 屏幕上运行此程序将生成一个简单的用户界面,在 PARAMETERS 语句中询问两个边界日期,就像 图 9-12 中显示的那样。

![SAP ABAP 程序屏幕,用于下载投诉数据。](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_0912.png)

###### 图 9-12\. SAP ABAP 程序屏幕,用于下载投诉数据

该代码生成一个文件,其条目如下,包括文档 ID 和投诉的文本:

5507234,"When the order was delivered, I could see the box was clearly mis- handled. It is really up to you to make sure the product gets to me without
being destroyed...I want a full shipping refund!"


# 分析数据

Jeana 和她的客服团队希望经常性地对这些数据进行分析,因此我们将专注于一种分析方法,简单地为他们提供一个基于从文本中提取的情感排序的客户投诉优先级列表。我们将使用刚刚查看的程序收集当天的投诉,将它们存储在 CSV 文件中,然后使用 Python 逐个投诉进行循环,并请求 Google 的自然语言 API 进行情感分析。

首先确保您使用了从“准备云 API”下载的凭据:

$ export GOOGLE_APPLICATION_CREDENTIALS='[YOUR_PATH_TO_FILE]'


接下来,我们调用 API 为每个笔记提取情感值。情感分析返回两个值:情感和强度。情感分数从-1.0(完全负面情感)到+1.0(完全正面情感)不等。强度分数表示文本与该情感的强烈关联程度,范围从 0 到无穷大。

Get the right Google Cloud stuff imported.

from google.cloud import language
from google.cloud.language import enums
from google.cloud.language import types
import pandas as pd

comp = pd.read_csv('example.csv', names=['document', 'complaint'])

A LanguageServiceClient handles the interchange

between computer and Google services.

lsc = language.LanguageServiceClient()

We are going to add the sentiment score and magnitude

to the dataframe as we process them.

score = []
magnitude = []
for index, row in comp.iterrows():
# Create a document for the request
lsc_doc = types.Document(
content=row['complaint'],
type=enums.Document.Type.PLAIN_TEXT)

# Send the document to be analyzed.
sentiment = lsc.analyze_sentiment(document=lsc_doc).document_sentiment

# Push the sentiment score and magnitude into lists for later.
score.append(round(sentiment.score, 2))
magnitude.append(round(sentiment.magnitude, 2))

After we finish processing them all, add 'score' and

'magnitude' columns.

comp['score'] = score
comp['magnitude'] = magnitude


我们还没有完全完成。对于每个查询的文档,我们都有情感分数和强度,但在我们给 Jeana 提建议之前,让我们再深思一下。

考虑到在 Big Bonanza 网站上提出的这个投诉:“Big Bonanza 是有史以来最糟糕的公司。” Google 评估此声明的情感为-0.9:极其负面。但强度为 0.9——请记住,强度不是从-1 到+1 的范围,而是从 0 到无穷大!Jeana 可能不希望将预算的任何部分分配给特别关注此投诉,因为这并不具体。给此人星巴克礼品卡或其他形式的奖励可能是无用的。他们甚至可能没有订购任何产品。显然,强度影响了原始情感分数的总体有用性。

现在考虑这个投诉:“我认为这个电池驱动的咖啡杯应该改进,因为它不能正确加热我的饮料。我想退货。” Google 对其情感评分为-0.2,强度为 0.5。在这种情况下,评论者似乎没有高负面情绪,而小的强度指数表明文本中也有些积极因素。根据我们的阅读和评分,Jeana 可能不希望为此投诉分配任何预算以作激励,因为情感分数表明此人并没有非常生气。

针对这个:“我的迷你微波炉一插上电就停止工作了。在第一次加热后不知怎么地门脱落了,然后修理了铰链后再也无法启动。这个产品不好。我要全额退款,包括运费!” Google 给出的情感评分为-0.5,强度为 2.1。Jeana 可能应该考虑对此人进行奖励,因为他们的情感评分明显处于负面范围,并且强度高于其他投诉。这个人似乎感到不满,并且在评论中表达了一些支持。

Duane 设置了几个初始参数,让 Jeana 可以处理数据。根据他从这些示例数据中观察到的情况,他过滤掉了原始分数低于-2.5 的投诉,以及不具有至少 1.5 数量级的投诉。他将剩余的投诉分别按照原始分数和数量级排序,并显示给 Jeana 审阅:

Create a dataframe that filters out the higher scores and

lower magnitudes.

filtered = comp.loc[(comp['score'] < -0.25) & (comp['magnitude'] > 1.5)]

Create separate dataframes that order differently.

sort_score = filtered.sort_values(by='score')
sort_magnitude = filtered.sort_values(by='magnitude', ascending=False)

Print them both out for director review.

print('Complaints weighing in as most heavily negative: ')
print(sort_score[['document', 'score', 'magnitude']].head())

print('Complaints with more total negative magnitude: ')
print(sort_magnitude[['document', 'score', 'magnitude']].head())


结果在打印输出中显示如下:

Complaints weighing in as most heavily negative:
document score magnitude
10 7093024 -0.9 1.9
31 7065438 -0.8 2.1
16 7034597 -0.8 2.3
75 7084738 -0.7 2.0
22 7071324 -0.7 3.1

Complaints with more total negative magnitude:
document score magnitude
52 7060923 -0.4 4.3
99 7092489 -0.5 4.1
77 7065486 -0.3 3.8
32 7098254 -0.5 3.6
44 7060766 -0.4 3.3


# 总结

自然语言处理(NLP)使计算机能够以前几年无法实现的方式处理人类语言。通过在大量人类语言数据集上训练模型,提供 NLP API 的云公司在 NLP 的通用方法上拥有独特优势。现在可以快速轻松地获得高质量的文本情感分析。

在这一章中,我们帮助了客户服务总监 Jeana,应用 NLP 来建议最具可操作性的客户投诉。我们通过使用 Google 的预训练、云启用的 API,快速分析个别投诉的人类情感,强调了交付速度。通过集中关注得分低的投诉,Jeana 可以在有限的福利预算上努力改善与可能流失的客户的关系,甚至避免其在社交媒体上损害 Big Bonanza。

我们特意选择了一个云 API 的例子,来向 SAP 分析师和数据科学家们强调:你们的工作并不总是要重新发明基础算法。在听取 Jeana 的需求后,Duane、Greg 和 Paul 一致认为在这里使用云 API 是最佳方案。Google Cloud Platform 没有“解决大收获问题的 API”,但当大收获的员工运用创造性思维、SAP 知识和数据科学时,他们无法被阻止!

¹ 示例取自 Porter 的算法:[*http://snowball.tartarus.org/algorithms/porter/stemmer.html*](http://snowball.tartarus.org/algorithms/porter/stemmer.html)。


# 第十章:结论

随着这一章的结束,我们的旅程也告一段落。我们在此道别,并祝愿您在继续学*数据科学和 SAP 的旅途中一切顺利。作为总结,我们想重新审视最初的使命,回顾前九章的内容,给您一些建议和推荐,并最终提供保持联系的途径。

# 最初的使命

我们曾经被承诺过从自动驾驶汽车(尽管有进展,但尚未广泛制造)到我们爱上的人工智能(如电影《Ex Machina》和《Her》中所描绘的那样)。我们还被警告说,我们将被我们自己创造的东西所取代,这将导致一个阴郁和荒凉的未来。这些相反的愿景削弱了数据科学的实际价值。人工智能和数据科学领域在其历史上经历过多次寒冬。这些是被狂热宣传后紧随其后的失望和兴趣流失的时期。不幸的是,有人推测我们正在进入甚至目前已经处于另一次兴趣衰退期。我们希望在本书中展示简单机器学*方法如何为企业数据提供即时价值,特别是在使用 SAP 数据时。数据科学和人工智能并不是被过度宣传了,而是被低估了。

我们在本书中最想做的事情之一是在商业分析师和数据科学家之间架起一座桥梁。商业分析师通常对其公司的数据和业务流程有清晰的理解。然而,他们缺乏数据科学的视角。数据科学家对建模和分析数据有清晰的方法。然而,他们经常缺乏业务流程的理解。您可能已经看到了流行的数据科学用例示例,如图 10-1 所示。这是一个机器学*或深度学*被要求识别图片是否是吉娃娃还是蓝莓松饼的场景。有了这样的场景,难怪商业分析师很难理解数据科学如何应用于企业数据。我们希望能够展示数据科学不仅仅是图像识别和吉娃娃。

###### 注意

本书中没有图像识别的示例是有原因的。虽然它可能是数据科学和人工智能最常被引用的例子,但在企业数据中很少必要。

![这是吉娃娃还是蓝莓松饼?这如何应用于业务?](https://github.com/OpenDocCN/ibooker-ds-zh/raw/master/docs/prac-ds-sap/img/pdss_1001.png)

###### 图 10-1:这是吉娃娃还是蓝莓松饼?这如何应用于业务?

# 总结

## 第一章:引言

我们介绍了架起企业数据和数据科学之间鸿沟的概念。我们还解释了用数据讲述故事的基本概念(和乐趣)。在这一章中,我们首先向数据科学家介绍了 SAP 的概念,并向 SAP 业务分析师介绍了数据科学的概念。

## 第二章:SAP 专业人士的数据科学

这一章是为 SAP 业务分析师准备的,介绍了许多数据科学术语。我们探讨了从机器学*和深度学*中探索概念的想法,目的是为业务分析师提供基础,预告未来即将到来的内容。

## 第三章:面向数据科学家的 SAP

本章的重点是数据科学家。SAP 拥有丰富的数据,但 SAP 是什么,那里存放着什么样的数据,以及如何获取这些数据?本章提供了这些问题的答案。

## 第四章:探索性数据分析

用数据科学的视角探索 SAP 数据的基础是探索性数据分析(EDA)。在本章中,我们介绍了从数据科学角度查看 SAP 数据的概念。最后,我们对数据进行了建模并失败了。这代表了一个重要的发现——有时候机器和深度学*模型无法回答我们的问题。本章的教训是要理解我们的投资是有限的,并继续探索其他数据科学的想法。

###### 注意

我们详细讨论了数据科学在业务场景中的失败问题。然而,最终我们决定,“这是现实生活”,并将其留作示例。

## 第五章:使用 R 和 Python 进行异常检测

这是一个内容丰富、涵盖了许多不同概念的重要章节。我们展示了如何使用 NetWeaver Gateway 提取数据,使用 Visual Studio 自动化该功能,将数据存储在 SQL 数据库中,使用 R 和 Python 对数据建模,并最终使用 PowerBI 报告发现。我们展示了所有这些技术如何协同使用,在寻找异常时可以产生令人印象深刻的初步结果。

## 第六章:使用 R 进行预测

本章的目标是对销售数据进行预测。我们使用 R 和 Python 创建了预测示例。我们再次使用 NetWeaver Gateway 提取数据,但这次我们进行了增强。然而,这只是一个开始,还有许多工作可以做。

## 第七章:使用 R 进行聚类和分割

客户是 SAP 数据中重要但常常被忽视的部分。本章旨在根据其购买*惯对客户进行聚类和分段。我们展示了如何使用多种机器学*技术(如*k*-means、*k*-medoids、层次聚类和手动聚类)来完成这一任务。有时候,讲述关于结果的好故事被忽视了,因此我们展示了如何使用 R Markdown 来提供令人印象深刻的报告。

## 第八章:关联规则挖掘

在本章中,我们将我们的数据科学调查操作化。关联规则是发现客户购买模式的常见技术。我们使用简单的程序提取了 SAP 数据,使用 R 创建了关联规则,创建了这些规则的 API,然后在 SAP Fiori 应用程序中使用了结果。这展示了使用 Fiori 向用户交付操作化模型有多么容易。在这种情况下,操作化模型很可能在创建销售订单时带来附加销售。

## 第九章:使用 Google Cloud 自然语言 API 进行自然语言处理

本章向商业用户介绍了可公开获取的 Google Cloud API。我们提供了一个情感分析的场景,几乎不需要进行模型编程。公开可用的 API 使用起来非常简单,几乎像是作弊代码一样。作为狂热的开发者,这是一章很难写的内容。我们喜欢编码,而不仅仅是访问 API。然而,从商业角度来看,如果 API 适合解决方案,通常经济选择是使用它。

# 提示和建议

如果经验教会了我们任何东西,那就是这三个原则:创造性、实用性和享受旅程。

## 充满创意

SAP 和其他形式的企业数据通常易于访问和清理。这些数据就像黄金矿藏一样宝贵。了解数据科学基础的业务分析师在利用这些数据方面处于一个很好的位置。在接收业务需求或项目请求时,想象一下本书中的例子,并尝试将其应用到您的情况中。业务不会要求您检测异常或创建关联规则。您是桥梁。对数据和数据科学的创新思维将引导您获得有益的解决方案。

## 保持实际

“如果你建造它...他们会来。”这是数据科学咨询公司的一种常见信念。要做数据科学,您需要一个 Hadoop 集群(也许需要几个),Spark、一个摄入引擎、17 名 R 程序员以及一位拥有博士学位的业务分析主任。我们见过一些公司在这种思维上花费数百万美元,最终一无所获。要在公司播种数据科学的种子,您需要一台计算机,您需要一种编程语言如 R 或 Python,您需要了解您的数据,并且您需要创新。数据科学是有益且有趣的。不要让它陷入自己的基础设施和术语的泥沼中。也许以后您会需要 Hadoop,也许以后您会想要 Spark 或 Cassandra;在到达那座桥之前先过好眼前的桥。现在,保持实际,并使用您已经拥有的工具。

## 享受这段旅程

数据科学在我们合计 40 年的 IT 经历中具有挑战性和超乎想象的奖励性。朝着您感兴趣的方面迈进:关联规则挖掘、异常检测、预测与预测,甚至是深度神经网络建模。对我们来说,受自然启发的算法尤其吸引人。我们希望您能像我们一样享受这段旅程!

# 保持联系

这只是我们旅程的开始,而不是结束。我们将继续发布博客来增强这本书......我们已经开始了。您对数据有什么样的想法?本书是否引导您进行了您想要分享的切线旅程?是否有什么还不清楚或者您被卡住了?好吧,现在您只能靠自己了。祝好运。

开个玩笑。我们随时都在,并真诚期待听到您的消息。您可以通过以下任何方式联系我们:

格雷格

+   电子邮件:gregfoss@bluedieseldata.com

+   推特:@bluedieseldata

+   领英:[*https://www.linkedin.com/in/greg-foss*](https://www.linkedin.com/in/greg-foss)

保罗

+   电子邮件:paul@paulmodderman.com

+   推特:@PaulModderman

+   领英:[*https://www.linkedin.com/in/paulmodderman/*](https://www.linkedin.com/in/paulmodderman/)
posted @ 2025-11-19 09:21  绝不原创的飞龙  阅读(17)  评论(0)    收藏  举报