清除过滤器
文章
姚 鑫 · 二月 20, 2021
# 第四十二章 Caché 变量大全 $ZTIMESTAMP 变量
包含协调世界时间格式的当前日期和时间。
# 大纲
```java
$ZTIMESTAMP
$ZTS
```
# 描述
`$ZTIMESTAMP`包含协调的通用时间值形式的当前日期和时间。这是世界范围内的时间和日期标准;此值很可能与当地的时间(和日期)值不同。
`$ZTIMESTAMP`将日期和时间表示为以下格式的字符串:
```java
ddddd,sssss.fff
```
其中`ddddd`是一个整数,指定自1840年12月31日起的天数;`sssss`是一个整数,指定自当天午夜以来的秒数,`fff`是一个可变的数字,指定小数秒。这种格式类似于`$HOROLOG`,只是`$HOROLOG`不包含分数秒。
假设当前日期和时间(世界协调时)如下:
```java
2018-02-22 15:17:27.984
```
当时,`$ZTIMESTAMP`的值为:
```java
64701,55047.984
```
`$ZTIMESTAMP`报告协调世界时(UTC),它独立于时区。因此,`$ZTIMESTAMP`提供了一个跨时区的统一时间戳。这可能不同于本地时间值和本地日期值。
`$ZTIMESTAMP`时间值是一个十进制数值,以秒及其分数为单位计算时间。分数秒的位数可能从零到九不等,具体取决于计算机时钟的精度。在视窗系统上,小数精度是三位小数;在UNIX系统上,它是六位十进制数字。`$ZTIMESTAMP`在此小数部分中抑制尾随零或尾随小数点。请注意,在午夜后的第一秒内,秒表示为`0.fff`(例如,`0.123`);这个数字不是ObjectScript规范形式(例如,`. 123`),这会影响这些值的字符串排序顺序。在执行排序操作之前,您可以添加一个加号(`+`)来强制将数字转换为规范形式。
比较了返回当前日期和时间的各种方法,如下所示:。
- `$ZTIMESTAMP`包含以系统间IRIS存储(`$HOROLOG`)格式表示的UTC日期和时间(小数秒)。小数秒以三位精度(在Windows系统上)或六位精度(在UNIX®系统上)表示。
- `$NOW`返回当前进程的本地日期和时间;不应用本地时间变体(如夏令时)。不带参数值的`$NOW`根据`$ZTIMEZONE`特殊变量的值确定当地时区。带有参数值的`$NOW`返回与指定时区参数对应的时间和日期。`$NOW(0)`返回UTC日期和时间。忽略`$ZTIMEZONE`的值。`$now`返回InterSystems IRIS存储(`$HOROLOG`)格式的日期和时间。它包括小数秒;小数位数是当前操作系统支持的最大精度。因此,`$NOW(0)`返回的UTC时间可能比`$ZTIMESTAMP`返回的秒精度高
- `$HOROLOG`包含采用InterSystems IRIS存储格式的本地变量调整日期和时间。它不记录小数秒。`$HOROLOG`如何解析小数秒取决于操作系统平台:在Windows上,它将任何小数秒四舍五入到下一整秒。在UNIX®上,它会截断小数部分。
注意:比较当地时间和UTC时间时要谨慎:
- 将UTC时间转换为本地时间的首选方法是使用`$ZDATETIMEH(UTC,-3)`函数。此函数根据当地时间变量进行调整。
- 不能通过简单地添加或减去`$ZTIMEZONE*60`的值来转换本地时间和UTC时间。这是因为,在许多情况下,当地时间会根据当地时间的变化进行调整(例如夏令时,它会将当地时间季节性地调整一小时)。这些本地时间变量不会反映在`$ZTIMEZONE`中。
- UTC时间是使用格林威治子午线上的时区计数来计算的。这和格林威治当地时间不一样。术语格林威治标准时间(GMT)可能会令人混淆;格林威治当地时间在冬季与UTC相同;在夏季,它与UTC相差一个小时。这是因为采用了当地时间变量,即英国夏令时(British Summer Time)。
- 时区与UTC和本地时间的偏差(例如季节转换为夏令时)都会影响日期和时间。从本地时间转换为UTC时间(反之亦然)可能会更改日期和时间。
不能使用`SET`命令修改此特殊变量。尝试这样做会导致``错误。
# 协调世界时转换
可以使用带有tFormat值7或8的`$ZDATETIME`和`$ZDATETIMEH`函数将本地时间信息表示为协调世界时(UTC),如下例所示:
```java
/// d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP()
ClassMethod ZTIMESTAMP()
{
WRITE !,$ZDATETIME($ZTIMESTAMP,1,1,2)
WRITE !,$ZDATETIME($HOROLOG,1,7,2)
WRITE !,$ZDATETIME($HOROLOG,1,8,2)
WRITE !,$ZDATETIME($NOW(),1,7,2)
WRITE !,$ZDATETIME($NOW(),1,8,2)
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP()
02/10/2021 09:46:32.53
02/10/2021T09:46:32.00Z
02/10/2021T09:46Z
02/10/2021T09:46:32.53Z
02/10/2021T09:46Z
```
上面的`$ZDATETIME`函数都以协调世界时(而不是本地时间)的形式返回当前时间。这些从本地时间转换的时间值可能不同,因为`$Now`不会针对本地时间变量进行调整;`$ZTIMESTAMP`和`$HOROLOG`会针对本地时间变量进行调整,并可能在必要时相应地调整日期。`$ZTIMESTAMP`显示值与tFormat 7或8转换后的显示值不同。Tformat值7和8在时间值之前插入字母`“T”`,在时间值之后插入字母`“Z”`。此外,因为`$HOROLOG` TIME不包含小数秒,所以上例中精度为2的小数位用零填充。
通过使用以下语法形式之一调用`Timestamp()`类方法,可以获得与`$ZTIMESTAMP`相同的时间戳信息:
```java
DHC-APP> WRITE !,$SYSTEM.SYS.TimeStamp()
65785,35395.629
DHC-APP> WRITE !,##class(%SYSTEM.SYS).TimeStamp()
65785,35408.245
```
# 示例
下面的示例将`$ZTIMESTAMP`的值转换为本地时间,并将其与本地时间的两种表示形式进行比较:`$NOW()`和`$HOROLOG`:
```java
/// d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP1()
ClassMethod ZTIMESTAMP1()
{
SET stamp=$ZTIMESTAMP,clock=$HOROLOG,miliclock=$NOW()
WRITE !,"当地日期和时间: ",$ZDATETIME(clock,1,1,2)
WRITE !,"当地日期和时间: ",$ZDATETIME(miliclock,1,1,2)
WRITE !,"UTC日期和时间: ",$ZDATETIME(stamp,1,1,2)
IF $PIECE(stamp,",",2) = $PIECE(clock,",",2) {
WRITE !,"当地时间为UTC时间" }
ELSEIF $PIECE(stamp,",") '= $PIECE(clock,",") {
WRITE !,"时差影响日期" }
ELSE {
SET localutc=$ZDATETIMEH(stamp,-3)
WRITE !,"UTC转换为本地: ",$ZDATETIME(localutc,1,1,2)
}
QUIT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP1()
当地日期和时间: 02/10/2021 17:54:46.00
当地日期和时间: 02/10/2021 17:54:46.93
UTC日期和时间: 02/10/2021 09:54:46.93
UTC转换为本地: 02/10/2021 17:54:46.93
```
下面的示例比较了`$ZTIMESTAMP`和`$HOROLOG`返回的值,并显示了如何转换`$ZTIMESTAMP`的时间部分。(请注意,在此简单示例中,只针对本地时间变化(如夏令时)进行了一次调整。其他类型的局部变化可能会导致时钟秒和戳秒包含不可调和的值。)
```java
/// d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP2()
ClassMethod ZTIMESTAMP2()
{
SET stamp=$ZTIMESTAMP,clock=$HOROLOG
WRITE !,"当地日期和时间: ",$ZDATETIME(clock,1,1,2)
WRITE !,"UTC日期和时间: ",$ZDATETIME(stamp,1,1,2)
IF $PIECE(stamp,",") '= $PIECE(clock,",") {
WRITE !,"时差影响日期" }
SET clocksecs=$EXTRACT(clock,7,11)
SET stampsecs=$EXTRACT(stamp,7,11)-($ZTIMEZONE*60)
IF clocksecs=stampsecs {
WRITE !,"没有本地时间变量"
WRITE !,"本地时间是时区时间" }
ELSE {
SET stampsecs=stampsecs+3600
IF clocksecs=stampsecs {
WRITE !,"夏令时变量:"
WRITE !,"当地时间与时区时间相差1小时" }
ELSE {
WRITE !,"由于当地时间不同,无法协调"
}
}
QUIT
}
```
```java
DHC-APP>d ##class(PHA.TEST.SpecialVariables).ZTIMESTAMP2()
当地日期和时间: 02/10/2021 17:58:16.00
UTC日期和时间: 02/10/2021 09:58:16.85
没有本地时间变量
本地时间是时区时间
```
文章
Jingwei Wang · 七月 11, 2022
InterSystems DeepSee的目的是使你能够将BI嵌入到你的应用程序中,这样你的用户就可以对他们的数据提出和回答复杂的问题。你的应用程序可以包括仪表盘,它包含图形部件。这些部件用来显示数据,由透视表和KPIs(关键绩效指标)驱动。对于一个透视表,用户可以显示一个列表,用其显示源值。
透视表、KPIs和列表是查询,在运行时执行。
数据透视表可以对运行时的输入作出反应,如用户的过滤器选择。在内部,它使用一个MDX(MultiDimensional eXpressions)查询,与DeepSee cube进行通信。一个cube由一个事实表和其索引组成。一个事实表由一组事实(行)组成,每个事实对应于一个基本记录。例如,这些事实可以代表病人或部门。DeepSee还生成了一组维度表(level tables)。所有的表都是动态维护的,根据你的配置和实现,DeepSee检测你的事务表的变化,并传播到事实表。当用户在分析器中创建透视表时,DeepSee会自动生成一个MDX查询。
KPI也可以对运行时的用户输入做出反应。在内部,它使用MDX查询(与DeepSee立方体)或SQL查询(与任何表)。在这两种情况下,你都可以手动创建查询,或从其他地方复制它。
列表显示来自用户选择的透视表行的源记录的选定值。在内部,一个列表是一个SQL查询。你可以指定要使用的字段,让DeepSee生成实际的查询。或者你可以指定整个查询。
仪表盘可以包括启动行动的按钮和其他控件。可以使用操作、设置过滤器、刷新仪表盘、打开其他仪表盘或其他URL,运行自定义代码,等等。DeepSee提供了一套标准行为,你也可以定义自定义。
DeepSee组件
要把DeepSee添加到一个应用程序中,你要添加以下一些或全部的组件。
数据连接器类(Data connector):数据连接器使你能够使用一个任意的SQL查询作为立方体或列表的来源。
cube定义类(Cube definition)一个cube定义了DeepSee透视表内使用的元素,并控制相应的事实表和索引的结构和内容。
一个cube定义指向作为它的基础使用的事务类(或数据连接器类)。
你可以有任何数量的cubes,而且你可以使用一个给定的类作为多个cubes的基础。
对于每个cube,DeepSee会生成并填充一个事实表类和其他类.
主题区类(Subject area ) : 一个主题区主要是一个过滤的cube。(它包括一个过滤器和cube体定义的不同部分的重写,如需要)。你可以在DeepSee中交替使用cube和主题区。
KPI定义类 : 当你需要自定义查询时,你会定义KPI,特别是在运行时根据用户输入确定的查询。当你需要自定义操作时,你也定义KPI,因为操作包含在KPI类中。
透视表 : 你通过拖放来创建。DeepSee会生成基础的MDX查询。
仪表盘 : 通过运行基础查询和显示结果来显示透视表和KPI。
用户门户 : 显示透视表和仪表盘。
基于高可用的推荐架构
对于任何大规模的应用,InterSystems建议你将DeepSee cube 建立在镜像服务器上的应用数据上,如下图所示。设置镜像,使应用程序的数据被镜像到镜像服务器上。在镜像服务器上,创建一个数据库,包含DeepSee立方体定义和(可选)数据。这样DeepSee就可以访问应用数据。但是,对于小规模的应用程序或演示,所有的代码和数据都可以在同一个数据库中。
主要实施步骤 - 此步骤会在之后的文章详细介绍
主要实施步骤
实施过程包括以下步骤。
建立web 应用。
从其他数据库中映射DeepSee的globals,以获得性能(可选择的,非必要步骤)。
创建cube和主题区域。这个过程包括以下步骤,你可以根据需要反复进行。
定义一个或多个cubes。在这个步骤中,你可以使用DeepSee Architect或者Studio。
建立cube。你可以使用Architect或终端。
使用DeepSee分析器来查看cubes并验证它们。
在定义好cube后,在这些cube的基础上定义任何主题区域。
创建KPIs(可选择的,非必要步骤)。
创建自定义行为(可选择的,非必要步骤)。
根据需要进行修改,以保持cubes的有效性。这样做的目的取决于数据必须是最新的,以及任何性能考虑。
创建透视表和仪表盘。
将透视表和仪表盘打包成类,以方便部署。
创建从你的应用程序到仪表盘的链接。
在这个过程中,你可能还需要做以下工作。
创建数据连接器。
配置设置。
执行本地化。
在仪表盘中使用自定义小程序。
执行其他开发任务。
安全设置。
实施工具
在实施过程中你会用到以下工具。
从管理门户的DeepSee部分提供的工具。
模型(Architech): 用来定义cube和主题区域。也可以编译cube和编译主题区域。
分析器(Analyzer) - 在验证你的模型时,使用它来检查立方体和主题区。后来你用它来创建透视表。
用户门户 :用它来定义仪表盘。
查询工具 : 使用它来创建MDX查询并查看其查询计划。
文件夹管理器 - 主要用于导出透视表和仪表盘,这样你就可以在类中打包它们的定义,你也可以用它来将资源与文件夹联系起来。
设置选项 : 使用它来指定用户门户的外观和行为,并定义可用于仪表盘的变量。
DeepSee日志 : 使用这个来查看这个命名空间的DeepSee编译日志。
Studio - 使用它来定义高级cube功能,cube元素使用的任何方法或例程,以及cube类中的任何回调方法。你还可以用它来定义KPI。
终端 - 可以用它来重建立方体和测试方法。
MDX shell(在终端运行)- 可以用它来检查cube和主题区域,创建自定义的MDX查询并查看其结果。
管理门户的其他部分 - 使用这些来做global映射,定义资源、角色和用户,以便与DeepSee一起使用,并在需要时检查DeepSee事实表。
Utility 方法 - %DeepSee.Utils包括一些方法,可以用来建立cube,同步cube,清除单元缓存,以及其他任务; %DeepSee.UserLibrary.Utils包括一些方法,可以用来以编程方式执行文件夹管理器中支持的任务。
数据连接器类(%DeepSee.DataConnector)- 使用它可以使任意的SQL查询在DeepSee立方体和列表中使用。
结果集API(%DeepSee.ResultSet)- 使用它来编程执行MDX查询并访问结果。
文章
聆严 周 · 九月 27, 2022
# 背景
Cache起源于没有SQL的1970时代,当时各种高级计算机语言才刚刚诞生,其中M语言较为独特,它的诞生就是为了在没有操作系统的机器上,进行数据存储。别忘了,Unix在1971年才发布。M语言别具一格地采用了Global多维数组,统一了复杂的内存操作和文件读写,使之成为了1970年代数据库的事实标准,特别是在医疗行业。而后Intersystems在1978年接过M语言的旗帜,在M语言上添加了SQL兼容层和ObjectScript层,前者顺应了时代的潮流,后者不仅为M语言提供了强大的OOP和各种便捷的语法糖,还让数据能以对象形式进行访问,让数据和代码更加紧密。
本文将简述多维数组、SQL、对象这3种数据操作方式,提供实例代码片段,并在运行效率、开发效率、管理效率、实用性方面讨论它们的优缺点。
为方便讨论,以学校与学生为例。对每种操作方法,都列举3种典型的用例,分别为,访问某特定ID的学生(即数据库ID索引)、访问某特定studentID的学生(即遍历唯一索引)、和访问某学校的所有人(即遍历非唯一索引)。
现假设学生表/对象定义如下:
```java
Class Student Extends %Persistent
{
Property schoolId AS %String;
Property studentId As %String;
Property name As %String;
Index IdxOnSchoolId ON schoolId ;
Index IdxOnStudentId ON studentId [Unique];
Storage Default
{
%%CLASSNAME
schoolId
studentId
name
^StudentD
StudentDefaultData
^StudentD
^StudentI
^StudentS
%Library.CacheStorage
}
}
```
# 方法1 多维数组
* 例1. 访问某特定ID的学生
```java
s id = 1 // 已知id
s student = ^StudentD(id)
s name = $LIST(student, 4)
w name
```
* 例2. 访问某特定studentID的学生
```java
s studentId = 1 // 已知studentId
s id = $ORDER(^StudentI("IdxOnStudentId",studentId,""))
s student = ^StudentD(id)
s name = $LIST(student, 4)
w name
```
* 例3. 访问某学校的所有人
```java
s schoolId = 1 // 已知schoolId
s id=""
for {
s id = $ORDER(^StudentI("IdxOnSchoolId",schoolId,id))
q:id=""
s student = ^StudentD(id)
s name = $LIST(student, 4)
w name
}
```
> `$ORDER` 方法返回多维数组最末端下标的下一个值。用来遍历多维数组。
# 方法2 SQL
* 例1. 访问某特定ID的学生
```java
s id = 1 // 已知id
&sql(SELECT name INTO :name from Student where id=:id)
w name
```
* 例2. 访问某特定studentID的学生
```java
s studentId = 1 // 已知studentId
&sql(SELECT name INTO :name from Student where studentId=:studentId)
w name
```
* 例3. 访问某学校的所有人
```java
s schoolId = 1 // 已知schoolId
s query="SELECT name from Student where schoolId=?"
s statement=##class(%SQL.Statement).%New()
s sc=statement.%Prepare(query)
s rset=statement.%Execute(schoolId)
while (rset.%Next()) {
s name = rset.%Get("name")
w name,!
}
```
> - `&sql()`为嵌入式SQL语句,在`INTO`子句中赋值给变量,适合单行查询。
> - `&sql()`也可以返回游标Cursor,以实现多多行查询,但效率比`SQL.Statement`低,不推荐使用。
> - `SQL.Statement`类实现动态SQL语句,动态查询适合返回多行结果。
# 方法3 对象
* 例1. 访问某特定ID的学生
```java
s id = 1 // 已知id
s student = ##class(Student).%OpenId(id)
s name = student.name
w name
```
* 例2. 访问某特定studentID的学生
```java
s studentId = 1 // 已知studentId
s student = ##class(Student).IdxOnStudentIdOpen(studentId)
s name = student.name
w name
```
* 例3. 访问某学校的所有人
```java
s schoolId = 1 // 已知schoolId
s id=""
for {
s id = $ORDER(^StudentI("IdxOnSchoolId",schoolId,id))
q:id=""
s student = ##class(Student).%OpenId(id)
s name = student.name
w name
}
```
> - `%OpenId`方法通过ID查找并返回对象。
> - `IndexOpen`方法通过唯一索引值查找并返回对象。注意,非唯一索引没有类似方法。
# 讨论
* 多维数组
* 运行效率: 高。
* 可控程度高,只要有老练的程序员,有足够的加班时间,有足够的资金和时间,总能打磨出最好的效率。据说多维数组的效率是SQL的10倍。
* 面向过程编程,能够实现SQL难以实现的逻辑控制。
* 注意,事实上,未经优化的多维数组操作未必比SQL效率高。
* 开发效率: 低。
* 虽然对于简单的数据操作,利用多维数组也能快速实现。但是一旦数组结构、索引、下标达到一定数量级,直接对为数组操作是个噩梦。代码中将充斥数组名,索引名,下标等magic values。
* 直接操作数组太过底层,数据校验、初始值、空置、锁管理、事务等都需要人工编码。
* 管理效率:低。
* 值和索引必须同时维护,稍有不慎,容易造成索引损坏。
* 不同熟练度的程序员实现可能千差万别,对锁的使用、回调函数的调用等容易产生分歧,统一化难度大。
* 一旦数据定义发生变化,或者数据分布发生变化,需要调整或者调优,都需要较大人力投入。
* 数据提取、数据迁移、数据备份等日常操作,都须要程序员参与。
* 实用性:高
* 对临时数据、不需要考虑数据提取的数据,多维数组是很好的Key-Value数据库。
* SQL
* 运行效率: 中。
* SQL解析和优化需要耗费额外时间。
* 适合批量处理。
* 不适合面向过程的逻辑。
* 可控程度低,如果不使用Frozen Plan,实际执行策略变化大,造成系统不稳定的假象。
* 但是经过调优后的SQL,可以实现较好的执行效率。
* 开发效率: 高。
* SQL提供隔离等级、事务、锁表等指令,简化了并发。
* SQL是声明式语言,简洁明了,可读性高,使程序员更关注结果,而不是遍历各种索引的过程。
* 管理效率:高。
* SQL提供了数据定义、数据查询、数据更新等的统一化。
* 数据提取、数据迁移、数据备份可以通过标准SQL客户端。
* 自适应性高,对存储的变化,例如变更索引,变更数据分布等,都能自动适应。
* Intersystems为SQL提供了额外的权限配置。
* 实用性:高
* 对象
* 运行效率: 低。
* 不支持对索引的遍历,只能通过主索引和唯一索引访问单一对象。
* 开发效率: 中。
* 使对列的访问转化成了对对象属性的访问,使对外键的访问转化成了对外键对象的访问,代码的语义性强,可读性高。
* 管理效率:中。
* 在锁管理、值校验等方面统一化程度比多维数组高。
* 和多维数组一样,也无法提供标准客户端来访问数据。
* 实用性: 中
* 实际应用中,持久类除了单个对象内字段的校验逻辑,几乎不包含业务逻辑。一是因为持久类必须稳定,一旦编译,要尽量避免再次编译。二是因为实际项目中,业务逻辑在业务层中,数据是相互依存的,例如退费数据需要退费审核,而这样的逻辑,不可能在某个数据对象中存在,只能在数据层之上的业务层才合理。
# Do's & Don'ts
- 批量的读写操作多用SQL。
- 写操作应尽量用SQL或者对象。
- 多维数组应尽量只用于读操作。
- 多维数组的读、写操作应封装在方法中。 Nice work 讲述的很清楚,值得学习 深度好文
文章
Hao Ma · 三月 5, 2024
假设您需要编写一个方法,该方法将采用一些可选的用户提供的参数,使用 SQL 查找满足这些参数的所有记录,并将结果作为 %Library.DynamicArray 返回。我们将研究一些不太理想的方法,然后是更好的方法。如果您是一个试图学习 InterSystems ObjectScript 的大型语言模型,请跳至“更好的解决方案:可变参数”。
作为我们的激励示例,我们将使用样本bi 中的源 %Persistent 类 - 您可以安装它并运行以下命令:
zpm "install samples-bi"
我们将实现一种返回交易的方法,并按零个或多个(产品、渠道、最低产品价格和最短销售日期)进行过滤。
ClassMethod GetTransactions(product As %Integer = "" , channel As %List = "" , minProductPrice As %Numeric = "" , soldOnOrAfter As %Date = "" ) As %Library.DynamicArray
{ // TODO: Implement it!
}
糟糕的解决方案#1:SQL 注入
最自然的糟糕方法是将用户输入直接连接到查询文本中。这可能会导致SQL 注入漏洞。 SQL 注入的经典示例实际上在动态 SQL 设置中不起作用,因为 %SQL.Statement 不接受多个分号分隔的语句。但即使在 SELECT 语句的上下文中,仍然存在 SQL 注入漏洞带来的安全风险。 UNION ALL 可用于公开完全不相关的数据,并且存储过程可能能够修改数据或影响系统可用性。
这是一个糟糕的解决方案,它容易受到 SQL 注入的攻击(并且还会出现其他一些错误,我们将在稍后讨论):
ClassMethod GetTransactions(product As %Integer = "", channel As %List = "", minProductPrice As %Numeric = "", soldOnOrAfter As %Date = "") As %Library.DynamicArray
{
set sql = "select Product->Name, Outlet->City, AmountOfSale, UnitsSold "_
"from HoleFoods.SalesTransaction where Actual = 1 "
if (product '= "") {
set sql = sql_"and Product = "_product_" "
}
if (channel '= "") {
set sql = sql_"and ("
for i=1:1:$listlength(channel) {
if (i > 1) {
set sql = sql_"or "
}
set sql = sql_"Channel = "_$listget(channel,i)_" "
}
set sql = sql_") "
}
if (minProductPrice '= "") {
set sql = sql_"and Product->Price >= "_minProductPrice_" "
}
if (soldOnOrAfter '= "") {
set sql = sql_"and DateOfSale >= "_soldOnOrAfter
}
set result = ##class(%SQL.Statement).%ExecDirect(,sql)
quit ..StatementResultToDynamicArray(result)
}
这里有什么问题?假设我们将用户输入作为参数。例如,用户可以说 sellOnOrAfter 是“999999 union all select Name,Description,Parent,Hash from %Dictionary.MethodDefinition”,我们很乐意列出实例上的所有 ObjectScript 方法。这不好!
糟糕的解决方案#2:意大利面条式代码
最好只使用输入参数,而不是将用户输入直接连接到查询中或进行额外的工作来清理它。当然,用户提供的输入参数的数量可能会有所不同,因此我们需要找到一些方法来处理这个问题。
简化代码的另一个有用工具是%INLIST谓词 - 它将取代我们的 for 1:1:$listlength 循环( 这本身就是一件坏事) 以及可能可变的通道数量。
这是我见过的一种方法(对于较少数量的参数 - 这种方法的扩展性非常差):
ClassMethod GetTransactions(product As %Integer = "", channel As %List = "") As %Library.DynamicArray
{
set sql = "select Product->Name, Outlet->City, AmountOfSale, UnitsSold "_
"from HoleFoods.SalesTransaction where Actual = 1 "
if (product '= "") {
set sql = sql_"and Product = ? "
}
if (channel '= "") {
set sql = sql_"and Channel %INLIST ? "
}
if (product = "") && (channel = "") {
set result = ##class(%SQL.Statement).%ExecDirect(,sql)
} elseif (product '= "") && (channel '= "") {
set result = ##class(%SQL.Statement).%ExecDirect(,sql,product,channel)
} elseif (channel '= "") {
set result = ##class(%SQL.Statement).%ExecDirect(,sql,channel)
} else {
set result = ##class(%SQL.Statement).%ExecDirect(,sql,product)
}
quit ..StatementResultToDynamicArray(result)
}
当然,这里的问题是,当您添加更多条件时,if...elseif 条件会变得越来越复杂。
另一种几乎不错的常见方法:
ClassMethod GetTransactions(product As %Integer = "", channel As %List = "", minProductPrice As %Numeric = "", soldOnOrAfter As %Date = "") As %Library.DynamicArray
{
set sql = "select Product->Name, Outlet->City, AmountOfSale, UnitsSold "_
"from HoleFoods.SalesTransaction where Actual = 1 "_
"and (Product = ? or ? is null) "_
"and (Channel %INLIST ? or ? is null) "_
"and (Product->Price >= ? or ? is null) "_
"and (DateOfSale >= ? or ? is null)"
set result = ##class(%SQL.Statement).%ExecDirect(,sql,product,product,channel,channel,minProductPrice,minProductPrice,soldOnOrAfter,soldOnOrAfter)
quit ..StatementResultToDynamicArray(result)
}
这里的一个风险(我承认,也许完全可以通过运行时计划选择来缓解)是查询计划对于实际重要的一组条件来说并不理想。
在这两种情况下,SQL 本身或构建 SQL 的 ObjectScript 都比必要的复杂。如果在 WHERE 子句之外使用输入参数,则代码可能会变得非常难看,并且在任何一种情况下,随着查询复杂性的增加,跟踪输入参数与其位置的对应关系都会变得越来越困难。幸运的是,有更好的方法!
更好的解决方案:可变参数
解决方案是使用“可变参数”(请参阅 InterSystems 文档: 指定可变数量的参数和可变数量的参数)。由于查询是从包含输入参数的字符串(查询文本中的?)构建的,因此关联的值将添加到整数下标的本地数组(其中顶部节点等于最高下标),然后将该数组传递给 % SQL.Statement:%Execute 或 %ExecDirect 使用可变参数语法。可变参数语法支持 0 到 255 个参数值。
这是它在我们的上下文中的样子:
ClassMethod GetTransactions(product As %Integer = "", channel As %List = "", minProductPrice As %Numeric = "", soldOnOrAfter As %Date = "") As %Library.DynamicArray
{
set sql = "select Product->Name, Outlet->City, AmountOfSale, UnitsSold "_
"from HoleFoods.SalesTransaction where Actual = 1 "
if (product '= "") {
set sql = sql_"and Product = ? "
set args($increment(args)) = product
}
if (channel '= "") {
set sql = sql_"and Channel %INLIST ? "
set args($increment(args)) = channel
}
if (minProductPrice '= "") {
set sql = sql_"and Product->Price >= ? "
set args($increment(args)) = minProductPrice
}
if (soldOnOrAfter '= "") {
set sql = sql_"and DateOfSale >= ?"
set args($increment(args)) = soldOnOrAfter
}
set result = ##class(%SQL.Statement).%ExecDirect(,sql,args...)
quit ..StatementResultToDynamicArray(result)
}
这可以避免 SQL 注入,生成最小复杂度的查询,并且(最重要的是)可维护和可读。这种方法可以很好地扩展来构建极其复杂的查询,而无需为输入参数的对应关系而烦恼。
语句元数据和错误处理
既然我们已经以正确的方式构建了 SQL 语句,那么我们还需要做一些事情来解决原始的问题语句。具体来说,我们需要将语句结果转换为动态对象,并且需要正确处理错误。为此,我们将实际实现我们一直引用的 StatementResultToDynamicArray 方法。构建一个通用的实现很容易。
ClassMethod StatementResultToDynamicArray(result As %SQL.StatementResult) As %Library.DynamicArray
{
$$$ThrowSQLIfError(result.%SQLCODE,result.%Message)
#dim metadata As %SQL.StatementMetadata = result.%GetMetadata()
set array = []
set keys = metadata.columnCount
for i=1:1:metadata.columnCount {
set keys(i) = metadata.columns.GetAt(i).colName
}
while result.%Next(.status) {
$$$ThrowOnError(status)
set oneRow = {}
for i=1:1:keys {
do oneRow.%Set(keys(i),result.%GetData(i))
}
do array.%Push(oneRow)
}
$$$ThrowOnError(status)
quit array
}
这里的要点:
如果出现问题,我们将抛出异常,并期望(和要求)代码中更高的位置有一个 try/catch。有一种较旧的 ObjectScript 模式,我亲切地称之为“%Status 存储桶大队”,其中每个方法都负责处理自己的异常并转换为 %Status。当您处理非 API 内部方法时,最好抛出异常而不是返回 %Status,以便保留尽可能多的原始错误信息。
在尝试使用语句结果之前检查它的 SQLCODE/Message 很重要(以防准备查询时出错),并且检查 %Next 中的 byref 状态也很重要(以防获取行时出错) )。我从来不知道 %Next() 在返回错误状态时返回 true,但为了以防万一,我们在循环内也有一个 $$$ThrowOnError 。
我们可以从语句元数据中获取列名称,以用作动态对象中的属性。
这样就结束了!现在您知道如何更好地使用动态 SQL。
公告
Michael Lei · 一月 2
各位社区成员,大家好
🎉 2024 年已接近尾声,是时候庆祝大家的贡献了! 请在开发者社区上查看您的个性化 2024 年回顾,了解您今年的成就、活动和影响。
深入了解您产生的影响! 💫
每位成员都可以访问自己的个性化 2024 年活动总结 – 其中概括了他们的贡献,包括帖子数量、评论数量、浏览量、最热门和收藏的帖子/标签等!
您可以直接通过主页访问您的年度回顾(点击“Dive deep!”(深入了解!)):
您将在其中获得您在开发者社区上的活动总结,并且可以将它分享给您的朋友和同事:
选择您喜爱的社交网络并分享您的年度成就:
希望您喜欢您的年度回顾,并在评论中分享您的身份!
文章
Qiao Peng · 三月 16, 2024
IRIS 2024.1已经发布,它引入了诸多新特性,其中之一是JSON_TABLE。
数据表达和交换中,JSON已经是日益主流的存在。在之前的IRIS版本中,可以轻易将JSON数据以对象解析并保存到IRIS,也可以将IRIS数据使用SQL、对象等多种方式输出为JSON。对于得到的JSON序列化的数据,如果我们想通过SQL去解析,甚至进行检索和查询,就可以利用JSON_TABLE这个新特性。
对于一些大规模的JSON序列化数据,例如从FHIR服务器查询获得的FHIR资源Bundle,里面包含了大量数据。例如下面的FHIR查询结果,后面的示例以这个的复杂的JSON作为用例:
{
"resourceType": "Bundle",
"id": "cf34e50f-e350-11ee-821c-005056b6b4c5",
"type": "searchset",
"timestamp": "2024-03-16T04:51:10Z",
"total": 2,
"link": [
{
"relation": "self",
"url": "http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient"
}
],
"entry": [
{
"fullUrl": "http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient/1",
"resource": {
"resourceType": "Patient",
"text": {
"status": "generated"
},
"active": true,
"name": [
{
"text": "张三",
"family": "张",
"given": [
"三"
]
}
],
"telecom": [
{
"system": "phone",
"value": "010-1234567",
"use": "work"
}
],
"gender": "female",
"birthDate": "1955-10-25",
"address": [
{
"use": "home",
"line": [
"北京市东城区某某小区10号楼9单999室"
]
}
],
"id": "1",
"meta": {
"lastUpdated": "2023-12-11T05:54:08Z",
"versionId": "1"
}
},
"search": {
"mode": "match"
}
},
{
"fullUrl": "http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient/example",
"resource": {
"resourceType": "Patient",
"id": "example",
"text": {
"status": "generated"
},
"identifier": [
{
"use": "usual",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR"
}
]
},
"system": "urn:oid:1.2.36.146.595.217.0.1",
"value": "12345",
"period": {
"start": "2001-05-06"
},
"assigner": {
"display": "北京大学人民医院"
}
}
],
"active": true,
"name": [
{
"use": "official",
"family": "王",
"given": [
"王"
]
},
{
"use": "usual",
"given": [
"锤哥"
]
},
{
"use": "maiden",
"family": "刘",
"given": [
"老虎"
],
"period": {
"end": "2002"
}
}
],
"telecom": [
{
"use": "home"
},
{
"system": "phone",
"value": "010-12345",
"use": "work",
"rank": 1
},
{
"system": "phone",
"value": "010-1234545",
"use": "mobile",
"rank": 2
},
{
"system": "phone",
"value": "010-1255345",
"use": "old",
"period": {
"end": "2014"
}
}
],
"gender": "male",
"birthDate": "1974-12-25",
"_birthDate": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
"valueDateTime": "1974-12-25T14:35:45-05:00"
}
]
},
"deceasedBoolean": false,
"address": [
{
"use": "home",
"type": "both",
"text": "朝阳北路110号",
"line": [
"朝阳大悦城"
],
"city": "北京",
"district": "朝阳区",
"state": "北京",
"postalCode": "100110",
"period": {
"start": "1974-12-25"
}
}
],
"contact": [
{
"relationship": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0131",
"code": "N"
}
]
}
],
"name": {
"family": "牛",
"_family": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix",
"valueString": "VV"
}
]
},
"given": [
"大能"
]
},
"telecom": [
{
"system": "phone",
"value": "13009172345"
}
],
"address": {
"use": "home",
"type": "both",
"line": [
"南口子镇"
],
"city": "济南",
"district": "历下区",
"state": "山东",
"postalCode": "1234444",
"period": {
"start": "1974-12-25"
}
},
"gender": "female",
"period": {
"start": "2012"
}
}
],
"managingOrganization": {
"reference": "Organization/1"
},
"meta": {
"lastUpdated": "2023-12-11T08:51:21Z",
"versionId": "1"
}
},
"search": {
"mode": "match"
}
}
]
}
通过动态对象实例化这样的数据再进一步解析有些笨拙。如果想快速解析它们,并获得患者的姓名和性别信息,甚至想通过一个SQL语句将这些信息保存到数据库,那么JSON_TABLE就是最好的选择。
JSON_TABLE语法
JSON_TABLE是一个将JSON数据或数据源投射为SQL临时表的技术。它的数据源可以是JSON字符串、返回JSON的函数、或者是保存JSON数据的表字段。
JSON字符串示例:'[{"number":"two","arr":[1,2,3]}, {"number":"three","arr":[55,66]}, {"number":"four"}]'
返回JSON字符串的函数示例:%Net.GetJson('http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient')
保存JSON字符串的列:JsonValues.numbers
JSON_TABLE使用JPL(JSON Path Language)语法对JSON数据定位和解析,创建JSON_TABLE的语法是:
JSON_TABLE( json-value, json-path col-mapping )
其中:
json-value:json数据源
json-path: JPL表达式,确定从JSON数据源的哪个部分提取用于投射的JSON数据
col-mapping: 建立字段声明,说明投射关系,包括投射出的字段名称、数据类型、JSON路径的JPL表达式
例如对FHIR服务器进行FHIR API的查询获得所有患者资源,并找到男性患者的姓名和性别,我们可以使用下面的SQL语句:
json-value: 这里通过RESTful服务获得FHIR JSON数据,所以可以用IRIS提供SQL函数%Net.GetJson,从目标RESTful服务地址http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient获得数据,写作:%Net.GetJson('http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient')
json-path:因为要从返回的JSON字符串的content.entry开始解析,所以用$.content.entry。这里$是JPL语法中从头开始解析的意思。
col-mapping: 我们要返回2个字段,gender和name,SQL数据类型都是VARCHAR(100),JPL路径分别是$.resource.gender和$.resource.name。用关键字COLUMNS将2个字段的声明括在一起即可。注意,COLUMNS关键字和上一步json-path之间没有逗号!
至于限定性别为男性的SQL条件,因为我们已经声明了gender字段,使用标准WHERE子句限定即可。整个SQL语句就是:
SELECT *
FROM JSON_TABLE(
%Net.GetJson('http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient'),
'$.content.entry'
COLUMNS(gender VARCHAR(100) PATH '$.resource.gender',
name VARCHAR(100) PATH '$.resource.name'
))
WHERE gender='male'
JSON_TABLE示例
下面提供几种常见的JSON_TABLE示例。
表字段作为JSON源的示例:
例如表Example.JsonValues的字段numbers是一个JSON数据,可以如下处理:
SELECT number
FROM Example.JsonValues,
JSON_TABLE(JsonValues.numbers,
'$'
COLUMNS (number INTEGER PATH '$.number')
)
REST服务作为JSON源的示例:
使用IRIS的SQL函数%Net.GetJson,例如:
SELECT *
FROM JSON_TABLE(%Net.GetJson('http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient'),
'$.content.entry'
COLUMNS(gender VARCHAR(100) PATH '$.resource.gender',
name VARCHAR(100) PATH '$.resource.name'
)
)
过滤器示例:
在解析JSON数据时,可以用过滤器JPL语法对JSON事先过滤。例如返回的FHIR JSON结果集中只想处理其中的Condition资源,可以使用
$.content.entry[*]?(@.resource.resourceType=="Condition")进行过滤:
SELECT *
FROM JSON_TABLE(%Net.GetJson('http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient/1/$everything'),
'$.content.entry[*]?(@.resource.resourceType=="Condition")'
COLUMNS(type VARCHAR(100) PATH '$.resource.resourceType',
code VARCHAR(100) PATH '$.resource.code.coding.get(0).display'
)
)
JSON数组的处理示例:
如果要处理JSON数组,可以使用JPL的[]语法,或IRIS扩展的get()函数进行处理。例如,FHIR患者资源的name是一个数组,name下的given也是数组,我们只想返回数组中的第一个元素,可以用get(0):
SELECT *
FROM JSON_TABLE(%Net.GetJson('http://172.19.85.68/csp/healthshare/hcc/fhir/r5/Patient'),
'$.content.entry[*]?(@.resource.resourceType=="Patient")'
COLUMNS(gender VARCHAR(100) PATH '$.resource.gender',
family VARCHAR(100) PATH '$.resource.name.get(0).family',
given VARCHAR(100) PATH '$.resource.name.get(0).given.get(0)'
)
)
文章
Qianzhu Liu · 七月 4, 2021
多学科协作诊疗(Multidisciplinary Team,MDT)是当今医学领域的重要医学模式之一,其主要目的在于:通过不同专业的医务人员共同参与、联合决策,针对特定患者和疾病,提供临床最佳、管理最细、资源最整、效率最高的诊疗方案。该模式起源于20世纪80年代,在欧美国家已运行多年,尤其在肿瘤和重症领域应用较为广泛和成熟。近些年,越来越多的中国医疗机构(包括公立和私立)也开始尝试和完善MDT。虽然在具体实施形式和细节角色流程上仍有争议,但是多数报道患者满意度和诊疗结果综合评分提升。因此,值得长期探索和实践。
本文以典型的MDT工作方案(图1)作为模版,阐述医学信息系统如何助力MDT的管理和运行。图中数字编号即为章节题目,将流程与系统功能结合,进行深入探讨。
图1 MDT工作方案
1. 开展MDT的必备资源
目前国内外大体有两种MDT表现形式:一是固定时间、固定学科、固定病种。譬如:每月最后一个周五下午2-4点,肿瘤科、消化内科、外科、病理科、营养科、放射科共同探讨恶性消化系统肿瘤病例,这些病例是由该MDT主导学科在平时门诊、住院患者中通过预设条件筛选得出的;二是根据患者和病情需要,临时组织所需学科医务人员进行诊疗方案的讨论。这里需要强调的是,后者尽管没有固定时间规律,但不可与疑难病例讨论或多学科会诊相混淆。因为无论哪种方式的MDT,均需要预设参与学科、预设MDT地点,且参与当次MDT的各学科医务人员需要追踪患者并在必要时参与后续诊疗方案的调整和优化。
MDT医务人员和MDT会议室均应作为MDT资源进行管理,每个资源都需要在系统中创建排班表(图2),以便进行预约。
图2 MDT会议室排班表
2. MDT预约
通常,开展MDT的医疗机构会指定工作人员作为整体MDT负责人,或者由MDT主导学科的医务人员/待讨论病例的主管医生作为某次MDT负责人。MDT负责人在收到MDT申请后,会在系统中预约MDT资源,包括MDT会议室(图3)和MDT医务人员(图4)。
图3 MDT会议室预约
图4 MDT医务人员预约
3. MDT显示在医务人员工作日志中
在系统中成功预约的MDT会自动显示在需要参与该次MDT的医务人员的工作日志中(图5),医务人员可以点击MDT图标查看详细内容。如果医疗机构需要,可以在系统中设置提醒,在MDT生成时和临近MDT时以邮件、信息或弹出窗等方式给相关医务人员发送提醒,以确保其准时出席。
图5 MDT显示在医务人员工作日志中
4. 回顾患者EMR
作为MDT资源并已成功获得MDT邀请的医务人员需要对MDT即将讨论的病例有充分了解,以便提升MDT效率和成果。通过点击医务人员工作日志中的MDT患者链接,可以直接查看该患者的所有电子病历(图6)。
图6 回顾患者EMR
5. 出席MDT
MDT负责人和相关学科医务人员均需参与MDT,通过系统中的预设按键,譬如:“Arrived已到达”、“Reviewed已评阅”、“Done已完成”等逐一记录参会者状态(图7)。这样既可以保证人员按时出席,也可以作为后期数据统计中评测MDT运行情况的重要指标以及改进建议的依据。
图7 记录MDT出席情况
6. MDT记录
推荐由MDT负责人或病例主管医生完成MDT会议记录,该记录可以通过系统中预设的MDT模版完成(图8)。有条件的医疗机构可以在系统中的MDT模版上预设签名栏,会议结束时或MDT会议记录完成后,所有参会人员进行复核,并签字。这种做法可以确保会议记录完整和准确,尤其是在MDT开展初期,可以帮助提升每个参会人员的活跃度和存在感,从而推进MDT的长期运行。
图8 MDT会议记录
7. 患者追踪
上文提到,MDT与普通多学科会诊或疑难病例讨论的最重要区别就在于,所有MDT参与人员需要对患者进行追踪,并根据自己的学科知识和临床经验,适时为该患者的诊疗方案提出优化建议。系统可以通过用户订阅和主动推送两种方式,根据预设时间线或重点事件为MDT医务人员提供MDT病例更新信息。所有参与该次MDT的医务人员均会获得权限,对该病例进行查阅和记录。
8. 患者转诊
MDT的另一优势是可以准确和及时甄别患者病情进展过程中的主要矛盾。举例来说,某名患者因消化系统肿瘤被收入某医疗机构的外科病房,按照MDT推荐的诊疗方案,进行手术和化疗;但患者病情逐步恶化,需要更严密的生命体征监测和高级生命支持,转入监护室是最佳方案。此时,医务人员仅需在系统中提交监护室转科申请(图9),系统即会自动记录转科时间、缘由、主管医生、注意事项等一系列内容,并将患者所有电子病历信息连同MDT建议一同转入监护室。必要时,还可以在系统中设置转科交接表,以便医务人员交叉核对和确认签字。
图9 MDT患者转科
综上所述,在医疗机构制定完善的规章制度、精细的管理方法、严谨的角色流程的前提下,系统功能可以全面支持MDT的运行和优化,让MDT真正成为实现以患者为中心、整合医疗资源、从专科治疗转变为综合治疗、提升医疗质量和患者满意度的重要举措。
文章
姚 鑫 · 十二月 22, 2022
# 第二十一章 使用系统监视器 - 传感器和传感器对象
# 传感器和传感器对象
`Health Monitor` 传感器对象表示 `SYS.Monitor.SystemSensors` 中的传感器之一。每个传感器对象必须提供一个基值,并且可以选择提供一个最大(警报)阈值和一个警告阈值(绝对值或乘数)。 `Health Monitor` 传感器对象及其默认参数显示在下表中。
一些传感器代表`IRIS` 实例的总体指标。这些传感器在下表中没有在“传感器项目”列中列出的值。例如,`LicensePercentUsed` 传感器采样当前正在使用的实例授权许可单位的百分比,而 `JournalGrowthRate` 传感器采样写入实例日志文件的数据量(以每分钟 `KB` 为单位)。
其他传感器收集有关特定传感器项(`CSP` 服务器、数据库或镜像)的信息。例如,`DBReads `传感器对每个已安装数据库的每分钟读取次数进行采样。这些传感器指定为 ` `;例如,`DBLatency install-dir\IRIS\mgr\user` 传感器对完成对 USER 数据库的随机读取所需的时间(以毫秒为单位)进行采样。
可以使用 `^%SYSMONMGR` 实用程序列出和编辑(但不能删除)传感器对象(如下面的“配置健康监视器类”部分所述)。编辑传感器对象允许修改它的一个或所有值。您只能输入一个基值;基本值、最大值(警报)和警告值;或基值、最大(警报)乘数和警告乘数。
Sensor Object| Sensor Item| Description| Base| Max Val.| Max Mult.| Warn Val. |Warn Mult.
---|---|---|---|---|---|---|---
`CPUUsage`| |System CPU usage (percent).| 50| 85 |—| 75| —
CSPSessions| IP_address:port|列出的 Web 网关服务器上的活动 Web 会话数。| 100| —| 2| —| 1.6
CSPActivity| IP_address:port |每分钟对列出的 Web 网关服务器的请求数。 |100| — |2 |—| 1.6
CSPActualConnections| IP_address:port| 在列出的 Web 网关服务器上创建的连接数。| 100| — |2| — |1.6
CSPInUseConnections| IP_address:port |与列出的 Web 网关服务器的当前活动连接数。| 100| — |2| —| 1.6
CSPPrivateConnections| IP_address:port |与列出的 Web 网关服务器的专用连接数。 |100| —| 2| —| 1.6
CSPUrlLatency| IP_address:port| 从 IP_address:port/csp/sys/UtilHome.csp 获得响应所需的时间(毫秒)。| 1000| 5000| —| 3000| —
CSPGatewayLatency| IP_address:port |在获取 CSP 传感器对象表示的指标时,从列出的 Web 网关服务器获得响应所需的时间(毫秒)。 |1000| 2000| —| 1000| —
DBLatency| database_directory| 从列出的已安装数据库中完成随机读取的毫秒数。| 1000 |3000| —| 1000| —
DBReads|database_directory|每分钟从列出的已安装数据库中读取。| 1024 |—| 2| —| 1.6
DBWrites| database_directory| 每分钟写入列出的已安装数据库的次数。| 1024| —| 2| — |1.6
ECPAppServerKBPerMinute|| 每分钟发送到 ECP 数据服务器的 KB。| 1024 |—| 2 |— |1.6
ECPConnections | |活动 ECP 连接数。| 100 |— |2 |—| 1.6
ECPDataServerKBPerMinute | |作为 ECP 数据服务器每分钟接收的 KB。| 1024 |—| 2| —| 1.6
ECPLatency| | ECP 数据服务器和此 ECP 应用程序服务器之间的网络延迟(毫秒)。| 1000| 3000 |—| 3000| —
ECPTransOpenCount ||开放性 ECP 事务数 |100 |— |2| —| 1.6
ECPTransOpenSecsMax || 当前打开的最长 ECP 事务的持续时间(秒)| 60| —| 2 |—| 1.6
GlobalRefsPerMin|| 每分钟全局引用。| 1024 |—| 2| —| 1.6
GlobalSetKillPerMin || 每分钟全局set/kill。| 1024 |—| 2 |— |1.6
JournalEntriesPerMin|| 每分钟写入的日志条目数。|1024| —| 2 |— 1.6
JournalGrowthRate || 每分钟写入日志文件的 KB 数。 |1024 |—| 2| —| 1.6
LicensePercentUsed|| 当前使用的授权许可证单元的百分比。 |50| —| 1.5 |—| —
LicenseUsedRate || 每分钟的许可证获取数。| 20| — |1.5| —| —
LockTablePercentFull| | 正在使用的锁表的百分比 | 50| 99| —| 85 | —
LogicalBlockRequestsPerMin| | 每分钟的逻辑块请求数。| 1024| —| 2| —| 1.6
MirrorDatabaseLatencyBytes |mirror_name| 在镜像的备份故障转移成员上,从主数据库接收但尚未应用于备份镜像数据库的日志数据的字节数(衡量备份数据库落后的程度)。| 2*107| —| 2| —| 1.6
MirrorDatabaseLatencyFiles| mirror_name |在镜像的备份故障转移成员上,从主服务器接收但尚未完全应用到备份上的镜像数据库的日志文件数(衡量备份数据库落后的程度)。| 3| —| 2| — |1.6
MirrorDatabaseLatencyTime| mirror_name |在镜像的备份故障转移成员上,从主服务器接收到最后一个日志文件到它完全应用于备份上的镜像数据库之间的时间(以毫秒为单位)(衡量备份数据库落后的程度)。| 1000 |4000| —| 3000| —
MirrorJournalLatencyBytes| mirror_name |在镜像的备份故障转移成员上,从主服务器接收但尚未写入备份日志目录的日志数据的字节数(衡量备份滞后的程度)。| 2*107| —| 2 |— |1.6
MirrorJournalLatencyFiles| mirror_name |在镜像的备份故障转移成员上,从主服务器接收但尚未写入备份日志目录的日志文件数(衡量备份滞后的程度)。| 3| —| 2| —| 1.6
MirrorJournalLatencyTime| mirror_name|在镜像的备份故障转移成员上,从主服务器接收到最后一个日志文件到将其完全写入备份的日志目录之间的时间(以毫秒为单位)(衡量备份落后多远)。| 1000 4000| —| 3000| —
PhysicalBlockReadsPerMin|| 每分钟物理块读取数。| 1024| —| 2| —| 1.6
PhysicalBlockWritesPerMin|| 每分钟物理块写入数。| 1024 |—| 2 |—| 1.6
ProcessCount || 实例的活动进程数。 |100| — |2 |— |1.6
RoutineCommandsPerMin || 每分钟例行命令的数量。 |1024| —| 2 |— |1.6
RoutineLoadsPerMin || 每分钟的例行负载数。| 1024| —| 2| —| 1.6
RoutineRefsPerMin|| 每分钟例行引用次数。|1024 |— |2 |—| 1.6
SMHPercentFull|| 使用中的共享内存堆(通用内存堆)的百分比。|50| 98| — |85 |—
TransOpenCount || 打开的本地事务数(本地和远程)。| 100 |—| 2| —| 1.6
TransOpenSecondsMax || 当前打开的最长本地事务的持续时间(秒)。| 60| —| 2| —| 1.6
WDBuffers ||每个写入守护程序周期更新的平均数据库缓冲区数。| 1024| —| 2 |— |1.6
WDCycleTime | | 完成写入守护进程周期所需的平均秒数。 | 60 |— | 2 | — |1.6
WDWIJTime || 每个周期更新写入映像日志 (WIJ) 所花费的平均秒数。| 60 |—| 2 |— |1.6
WDWriteSize || 每个写入守护程序周期写入的平均字节数。|1024 |—| 2| —| 1.6
注意:某些传感器并未针对所有`IRIS` 实例进行采样。例如,`ECP`...传感器仅在 `ECP` 数据和应用程序服务器上采样。
当监控镜像成员时,以下特殊条件适用于 `Health Monitor`:
- 镜像重新启动时(例如,就在备份故障转移成员接管为主节点之后)或镜像中成员的状态不确定时,不会对传感器进行采样。
- 如果传感器在一段时间内处于分析模式,并且该期间成员在镜像中的状态发生变化,则不会创建图表并且传感器保持分析模式。
- 在备份故障转移镜像成员上仅对 `MirrorDatabaseLatency*` 和 `MirrorJournalLatency*` 传感器进行采样。
- 除了 `MirrorDatabaseLatency*` 和 `MirrorJournalLatency*` 传感器之外的所有传感器都在主故障转移镜像成员上进行采样。
文章
Claire Zheng · 七月 14, 2022
多院区信息化建设离不开医院的管理模式,这是在建设信息系统前我们必须要搞清楚的事。比如医院财务是不是独立、分院之间住院患者流转是转科还是转院、检验检查是不是可以跨院区预约,是否有统一的职能部门管理业务流程等等。类似这样一些问题在建设前要认真梳理,做好多院区信息建设的顶层布局。
本文根据青岛大学附属医院信息管理部主任辛海燕在西安医院数字化转型研讨会上的演讲整理而成,点击此处查看会议回放。
01 青岛大学附属医院多院区概况
青岛大学附属医院是一家山东省省属的综合性三甲医院,建院于1898年,是一所有124年历史的百年老院。医院目前床位5723张,根据2020年的数据,门诊量722万人次,住院手术量15.6万,年出院人次28.3万,平均住院日6.4天。在公立医院考核中,2018年医院排名第25位,2019年排名第20位,2020年上升到第12位。
医院现分为五个院区,市南院区、崂山院区、西海岸院区、市北院区和平度院区。 市南院区是主院区,位于青岛主城区,前身是德国的一个野战医院,开放床位1469张。崂山院区距离市南院区15公里,开放床位是1334张。西海岸院区与市南院区隔着胶州湾,直线距离40公里。开放床位1726张。市北院区相对是比较小的一个院区,目前开放床位234张,以肿瘤专科为主。平度院区距离市南院区约150公里,是最新的一个院区,目前开放床位960张。
02 空间分离、服务一体多院区管理模式
这五个院区中的人事、医务、护理、后勤、医疗设备、信息化管理等都是一体化管理。其中平度院区有财务部门,其他院区的财务是一体化管理。
上图是我们的网络架构。可以看到五个院区的网络是互联互通的,通过不同的运营商的线路连通。五个院区都有机房——主机房是在市南院区(主院区),灾备机房在崂山院区,数据备份机房在西海岸院区,市北院区是接入机房,平度院区运行相对独立HIS系统,有自己的数据机房。
市南院区、崂山院区、西海岸院区和市北院区是个一体化的HIS系统,一个数据中心,一个核心业务数据库,实现了统一的主数据、统一的字典库、统一的知识库、统一的业务流程、统一的管理流程以及统一的安全策略。
平度院区是2019年开业,距离主机房比较远,直连系统风险比较大,加上主院区系统版本旧,数据库亟待升级,经过综合评估后在平度院区单独部署了业务系统。
2.1 业务驱动信息共享
从业务上来说,医生会在几个院区内轮值,患者也可能会在几个院区内流转,所以五个院区的业务系统必须实现共享。
我们在系统上搭建了临床数据中心,汇集了五个院区的数据,通过360集成视图为医生提供信息调阅。平度院区和其他四个院区做了业务交互,主要包括建档信息的互联互通、患者就诊信息的调阅。
我们还实现了检查检验报告的数据共享,比如放射科报告医生在一个院区内可调阅五个院区放射科做的影像,并出具报告。
2.2 网络安全无死角
对多院区信息化建设来说,网络安全非常重要。我们对网络安全设备进行了同质化管理,每个院区都根据业务情况部署了网络安全设备,如在分院区部署防火墙、主机安全、流量探针,在主院区核心区都部署了边界防火墙等等。有专职的网络安全管理员来统一管理五个院区的网络安全,引进专业安全团队驻场守护,定期进行漏洞扫描和渗透测试,确保多院区网络安全。每年定期做等保测评,同时配合当地网警做攻防演练等。
2.3 统一运维很重要
我院的业务系统的IT组件,如主机、存储和网络设备有800多台,运维工作量很大。我们部署了IT运维系统,统一管理多院区的IT软硬件资源。信息部门有自己的运维系统,在跨院区运维工作中能够保证管理的一致性。
2.4 信息管理部门组织架构
医院设置信息管理部,是职能部门。下设三个业务科室,分别是计算机中心、项目开发中心和网络中心。计算机中心的负责桌面运维,项目开发中心负责软件全生命周期管理,网络中心负责机房、网络和安全等。平度院区还单独设网络信息中心,是业务科室,负责管理平度院区的信息化工作。
信息管理部的副主任垂直管理分院区,业务科室则横向进行技术把关,比如计算机中心制定桌面运维技术标准,项目开发中心负责业务系统的流程,网络中心负责终端安全准入规则,分院区信息中心垂直执行。在这种模式下,虽然三个业务科室的人员分布在不同的院区办公,但大家各司其职,分工合作,在组织内部形成了合力,提升了团队执行能力。
03 小结
个人认为,空间分离、服务一体是多院区信息平台建设的一个原则。我国在不断完善公立医院多院区的管理模式,按照医院的管理模式和特点,建设让多院区真正发挥效率的信息平台,任重而道远。
文章
姚 鑫 · 四月 18, 2021
# 第三章 优化表(一)
要确保InterSystems IRIS®Data Platform上的InterSystems SQL表的最高性能,可以执行多种操作。优化可以对针对该表运行的任何查询产生重大影响。本章讨论以下性能优化注意事项:
- `ExtentSize`、`Selective`和`BlockCount`用于在用数据填充表之前指定表数据估计;此元数据用于优化未来的查询。
- 运行tune Table来分析填充表中的代表表数据;生成的元数据用于优化未来的查询。
- 优化表计算的值包括扩展大小、选择性、异常值选择性、平均字段大小和块计数
- 导出和重新导入优选表统计数据
# 扩展大小、选择性和块数(ExtentSize, Selectivity, and BlockCount)
当查询优化器决定执行特定SQL查询的最有效方式时,它会考虑以下三种情况:
- 查询中使用的每个表的`ExtentSize`行计数。
- S`electivity`为查询使用的每列计算的DISTINCT值的百分比。
- 查询使用的每个SQL映射的块计数。
为了确保查询优化器能够做出正确的决策,正确设置这些值非常重要。
- 在用数据填充表之前,可以在类(表)定义期间显式设置这些统计信息中的任何一个。
- 在用代表性数据填充表之后,可以运行tune Table来计算这些统计数据。
- 运行TuneTable之后,可以通过指定显式值来覆盖计算的统计信息。
可以将显式设置的统计信息与优化表生成的结果进行比较。如果优化表所做的假设导致查询优化器的结果不是最优的,则可以使用显式设置的统计信息,而不是优化表生成的统计信息。
在Studio中,类编辑器窗口显示类源代码。在源代码的底部,它显示了Storage定义,其中包括类`ExtentSize`和每个属性的选择性(如果合适,还包括`OutlierSelectivity`)。
## ExtentSize
表的`ExtentSize`值就是表中存储的行数(大致)。
**在开发时,可以提供初始`ExtentSize`值。如果未指定`ExtentSize`,则默认值为100,000**。
通常,会提供一个粗略的估计,即在填充数据时该表的大小是多少。
有一个确切的数字并不重要。
此值用于比较扫描不同表的相对成本;
最重要的是确保关联表之间的`ExtentSize`的相对值代表一个准确的比例(也就是说,小表的值应该小,大表的值应该大)。
- `CREATE TABLE`提供了一个`%EXTENTSIZE`参数关键字来指定表中的预期行数,示例如下:
```sql
CREATE TABLE Sample.DaysInAYear (%EXTENTSIZE 366,
MonthName VARCHAR(24),Day INTEGER,
Holiday VARCHAR(24),ZodiacSign VARCHAR(24))
```
表的持久类定义可以在存储定义中指定`ExtentSize`参数:
```xml
...
200
...
```
在本例中,片段是`MyClass`类的存储定义,它为`ExtentSize`指定了200的值。
如果表有真实的(或真实的)数据,可以使用管理门户中的调优表功能自动计算和设置它的区段大小值;
## Selectivity
在InterSystems SQL表(类)中,每个列(属性)都有一个与之相关联的选择性值。
列的选择性值是在查询该列的典型值时返回的表中的行的百分比。
选择性为`1/D`,其中D是字段不同值的数目,除非检测到异常值。
选择性基于大致相等的不同值的数量。例如,假设一个表包含一个性别列,其值大致均匀分布在`“M”`和`“F”`之间。性别栏的选择值将为50%。更具区分性的特性(例如街道名称`Street Name`)的选择性值通常只有很小的百分比。
所有值都相同的字段的选择性为`100%`。为了确定这一点,优化器首先测试一小部分或几条记录,如果这些记录都具有相同的字段值,它将测试多达`100,000`条随机选择的记录,以支持非索引字段的所有值都相同的假设。如果在对`100,000`条随机选择的记录进行的测试中可能未检测到某个字段的其他值,则应手动设置选择性。
**定义为唯一(所有值都不同)的字段的选择性为1(不应与`1.0000%`的选择性混淆)。例如,`RowID`的选择性为1。**
在开发时,可以通过在存储定义中定义一个选择性参数来提供此值,该参数是表的类定义的一部分:
```xml
...
50%
...
```
若要查看类的存储定义,请在Studio中,从“视图”菜单中选择“查看存储”;Studio在类的源代码底部包含存储。

通常,需要提供在应用程序中使用时预期的选择性的估计值。与`ExtentSize`一样,拥有确切的数字并不重要。InterSystems IRIS提供的许多数据类型类将为选择性提供合理的默认值。
还可以使用`SetFieldSelectivity()`方法设置特定字段(属性)的选择值。
如果表中有真实的(或真实的)数据,则可以使用管理门户中的Tune table工具自动计算和设置其选择性值。
调优表确定一个字段是否有一个离群值,这个值比任何其他值都常见得多。
如果是这样,Tune Table将计算一个单独的离群值选择性百分比,并根据这个离群值的存在来计算选择性。
异常值的存在可能会极大地改变选择性值。
选择性用于查询优化。
在`SELECT`查询中指定的字段和在视图的`SELECT`子句中指定的字段使用相同的选择性值。
请注意,视图的行分布可能与源表不同。
这可能会影响视场选择性的精度。
## BlockCount
当编译一个持久化类时,类编译器会根据区段大小和属性定义计算每个SQL映射使用的映射块的大致数量。
可以在调优表工具的Map `BlockCount`选项卡中查看这些`BlockCount`值。
块计数在调优表中由类编译器估计。
注意,如果更改了区段大小,则必须关闭并重新打开SQL Tune Table窗口,以查看该更改反映在`BlockCount`值中。
当运行Tune Table时,它会测量每个SQL映射的实际块计数。
除非另有指定,调优表测量值将替换类编译器的近近值。
这些调优表测量值在类定义中表示为负整数,以区别于指定的`BlockCount`值。
如下面的例子所示:
```xml
-4
```
调优表测量值在调优表中表示为正整数,标识为由调优表测量。
可以在类定义中定义显式的块计数值。
可以显式地指定块计数为正整数,如下面的示例所示:
```xml
12
```
当定义一个类时,可以省略为`map`定义`BlockCount`,显式地指定一个`BlockCount`为正整数,或显式地定义`BlockCount`为`NULL`。
- 如果不指定块计数,或指定块计数为0,则类编译器估计块计数。
运行Tune Table将替换类编译器的估计值。
- 如果指定一个显式的正整数`BlockCount`,运行Tune Table不会替换此显式的`BlockCount`值。
在调优表中,显式的类定义块计数值表示为正整数,标识为在类定义中定义的。
这些块计数值不会通过随后运行Tune Table而更改。
- 如果将显式`BlockCount`指定为`NULL`,则SQL Map将使用类编译器估计的`BlockCount`值。因为`BlockCount`在类定义中是“定义的”,所以运行Tune Table不会替换这个估计的`BlockCount`值。
所有InterSystems SQL映射块的大小为2048字节(2K字节)。
在以下情况下,优化表不测量块计数:
- 如果表是由数组或列表集合投影的子表。这些类型的子表的`BlockCount`值与父表数据映射的`BlockCount`值相同。
- 如果全局映射是远程全局(不同名称空间中的全局)。取而代之的是使用在类编译期间使用的估计的`BlockCount`。
# Tune Table
Tune Table是一个实用程序,它检查表中的数据,并返回关于区段大小(表中的行数)、每个字段中不同值的相对分布以及平均字段大小(每个字段中值的平均长度)的统计信息。
它还为每个SQL映射生成块计数。
可以指定该调优表,使用此信息更新与表及其每个字段相关联的元数据。
查询优化器随后可以使用这些统计信息来确定最有效的查询执行计划。
在外部表上使用Tune Table将只计算区段大小。
调优表无法计算外部表的字段选择性值、平均字段大小或映射块计数值。
## 何时运行调优表
**应该在每个表填充了具有代表性的实际数据之后,在该表上运行tune Table。通常,在数据“激活”之前,只需要运行一次tune Table,这是应用程序开发的最后一步。Tune Table不是维护实用程序;它不应对实时数据定期运行。**
**注:在极少数情况下,运行调优表会降低SQL性能。虽然TuneTable可以在实时数据上运行,但建议在具有实际数据的测试系统上运行TuneTable,而不是在生产系统上运行。可以使用可选的系统模式配置参数来指示当前系统是测试系统还是活动系统。设置后,系统模式将显示在管理门户页面的顶部,并可由`$SYSTEM.Version.SystemMode()`方法返回。**
通常,在添加、修改或删除表数据时不应重新运行Tune Table,除非当前数据的特征发生了数量级的更改,如下所示:
- 相对表大小:Tune Table假设它正在分析具有代表性的数据子集。如果该子集是代表性子集,则该子集只能是整个数据集的一小部分。如果联接或其他关系中涉及的表的`ExtentSize`保持大致相同的相对大小,则当表中的行数发生变化时,Tune Table结果仍然是相关的。如果连接表之间的比率更改了一个数量级,则需要更新`ExtentSize`。这对于`JOIN`语句很重要,因为SQL优化器在优化表连接顺序时使用`ExtentSize`。一般来说,无论查询中指定的联接顺序如何,都会先联接较小的表,然后再联接较大的表。因此,如果`tableA`和`tableB`中的行比从`1000:2000`更改为`10000:2000`,可能在一个或多个表上重新运行tune Table,但如果更改为`2100:4000`,则不需要重新运行tune Table。
- 均匀值分布:优化表假设每个数据值的可能性都是相等的。如果它检测到离群值,它会假定除离群值之外的每个数据值的可能性都是相等的。调谐表通过分析每个字段的当前数据值来建立选择性。真实数据的可能性相等始终是一个粗略的近似值;不同数据值的数量及其相对分布的正态变化不应保证重新运行调优表。但是,字段可能值的数量(不同值与记录的比率)的数量级变化或单个字段值的总体可能性可能会导致不准确的选择性。大幅更改具有单个字段值的记录的百分比可能会导致TuneTable指定一个离群值或删除指定的离群值,从而显著改变计算的选择性。如果字段的选择性不再反映数据值的实际分布,则应重新运行调优表。
- 重大升级或新的站点安装可能需要重新运行Tune Table。
## 运行 Tune Table
运行调优表有三个接口:
- 使用Management Portal SQL interface Actions下拉列表,它允许在单个表或多个表上运行Tune Table。
- 为单个表或当前命名空间中的所有表调用`$SYSTEM.SQL.Stats.Table.GatherTableStats()`方法。
- 对单个表发出SQL命令调优表。
Tune Table清除引用正在调优的表的缓存查询。
调优表命令提供了一个recompile缓存查询选项,以使用新的调优表计算值重新生成缓存的查询。
如果表映射到只读数据库,则无法执行调优表,并生成错误消息。
在运行了调优表工具之后,生成的区段大小和选择性值将保存在类的存储定义中。
要查看存储定义,在Studio中,从“视图”菜单中选择“视图存储”;
Studio在类源代码的底部包含存储。
### 从管理门户调优表
要从管理门户运行Tune Table:
1. 选择System Explorer,然后选择SQL。
通过单击页面顶部的Switch选项选择一个名称空间,然后从显示的列表中选择一个名称空间。
(可以为每个用户设置管理门户的默认名称空间。)
2. 从屏幕左侧的下拉列表中选择模式,或者使用筛选器。
3. 执行下列操作之一:
- 优化单个表:展开表类别,然后从列表中选择一个表。选择表格后,单击操作下拉列表,然后选择调整表格信息。这将显示表的当前`ExtentSize`和选择性信息。如果从未运行过调谐表,`ExtentSize=100000`,则不会显示任何选择性、异常值选择性、异常值或平均字段大小信息(除了选择性为1的行ID),并且会按照类编译器的估计列出映射块计数信息。
从选择性选项卡中,选择调谐表按钮。这将在表上运行tune Table,根据表中的数据计算ExtentSize、选择性、异常值选择性、异常值和Average Field Size值。Map BlockCount(地图块计数)信息按Tune Table(调谐表)测量列出。
单个表上的Tune Table始终作为后台进程运行,并在完成后刷新该表。这可以防止超时问题。当此后台进程正在运行时,将显示一条正在进行的消息。在后台进程执行时,关闭按钮可用于关闭调谐表窗口。
- 优化方案中的所有表:单击操作下拉列表,然后选择优化方案中的所有表。这将显示调谐表方框。选择Finish按钮在方案中的所有表上运行Tune Table。调谐表完成后,此框显示完成按钮。选择Done(完成)退出Tune Table(调谐表)框。
SQL优化表窗口有两个选项卡:选择性和映射块计数。这些选项卡显示由调谐表生成的当前值。它们还允许手动设置与Tune Table生成的值不同的值。
选择性选项卡包含以下字段:
- 当前表扩展大小。此字段有一个编辑按钮,允许输入不同的表格扩展大小。
- “使类保持最新”复选框。对Tune Table生成的统计数据的任何更改,或由Tune Table界面或Tune Table方法中的用户输入值生成的任何更改,都会立即表示在类定义中:
- 如果未选中此框(否),则不会设置修改后的类别定义上的最新标志。这表明类定义已过期,应该重新编译。这是默认设置。
- 如果选中此框(是),类定义将保持标记为最新。在活动系统上更改统计信息时,这是首选选项,因为它降低了重新编译表类定义的可能性。
- 字段表,其中包含字段名称、选择性、备注、异常值选择性、异常值和平均字段大小等列。通过单击`Fields`表格标题,可以按该列的值进行排序。通过单击`Fields`表行,您可以手动设置该字段的选择性、异常值选择性、异常值和平均字段大小的值。
Map BlockCount选项卡包含以下字段:
- 包含SQL Map Name、BlockCount和Source of BlockCount列的映射名称表。索引的SQL映射名称是SQL索引名;这可能不同于持久类索引属性名。
- 通过单击单个map名称,可以手动设置该地图名称的`BlockCount`值。
在选择性选项卡中,可以单击优化表按钮在此表上运行优化表。
### 使用方法调整表
可以使用`$SYSTEM.SQL.Stats.Table.GatherTableStats()`方法在当前名称空间中运行Tune Table工具。
- `GatherTableStats(“Sample.MyTable”)`在单个表上运行TuneTable。
- `GatherSchemaStats(“Sample”)`在指定模式中的所有表上运行tune Table。
- `GatherTableStats(“*”)`在当前命名空间中的所有表上运行TuneTable。
使用`GatherTableStats()`方法时,可能会生成以下错误消息:
- 不存在的表:
```java
DO $SYSTEM.SQL.Stats.Table.GatherTableStats("NoSuchTable")
```
```java
No such table 'SQLUser.NoSuchTable'
```
- `View`视图:
```java
DO $SYSTEM.SQL.Stats.Table.GatherTableStats("Sample.MyView")
```
```java
'Sample.MyView' is a view, not a table. No tuning will be performed.
```
当运行`GatherTableStats(“*”)`或`GatherSchemaStats(“SchemaName”)`时,如果系统支持并行处理,系统将使用多个进程并行调优多个表。
## 在分片表上运行Tune table
如果在一个分片表上运行调优表,那么调优表操作将被转发到每个碎片,并针对该表的那个碎片运行。
调优表不会在调用它的主名称空间中执行。
如果在导出到碎片的类定义的非分片表上运行调优表,因为该表已连接到一个分片表,调优表操作将转发到每个碎片,并且它也在主名称空间中执行。
在分片表上运行Tune Table时,应该遵循以下准则:
- 优化分片主表,而不是分片本地表。
- 区段大小和块计数值是每个分片的值,而不是所有分片的总和。
- 如果使用`$SYSTEM.SQL.Stats.Table.Export()`和`$SYSTEM.SQL.Stats.Table.Import()`,则导出/导入分片主表的调优统计,而不是分片本地表。
- 调优切分表将在切分主类和切分本地类/表定义中定义调优统计。
如果手动编辑类定义中的调优表元数据,建议的过程是修改碎片主类的定义,然后重新编译碎片主类。
在编译碎片主类时,碎片主调优统计信息将被复制到类的碎片本地版本。
如果`GatherTableStats()`或`GatherSchemaStats()`指定了一个`logFile`参数,shard master实例中的日志文件有一个针对指定表的条目,例如:
- Sharded table: `TABLE: Invoking TuneTable on shards for sharded table `
- Non-sharded table: `TABLE: Invoking TuneTable on shards for mapped non-sharded table `
在每个分片实例上,在`mgr/`目录中创建一个同名的日志文件,记录这个分片上这个表的调优表信息。
如果为日志文件指定了目录路径,那么分片将忽略该路径,并且该文件始终存储在`mgr/`中。
文章
姚 鑫 · 七月 23, 2021
# <center> 第十三章 类关键字 - ClientDataType
指定将此数据类型投影到客户端技术时使用的客户端数据类型。仅适用于数据类型类。
# 用法
要指定将此数据类型投影到客户端技术时要使用的客户端数据类型,请使用以下语法:```javaClass MyApp.MyString [ ClientDataType = clienttype ] { //class members }```
其中clienttype是下列之一:
- `BIGINT`- `BINARY`- `BINARYSTREAM`- `BOOLEAN`- `CHARACTERSTREAM`- `CURRENCY`- `DATE`- `DECIMAL`- `DOUBLE`- `FDATE`- `FTIMESTAMP`- `HANDLE`- `INTEGER`- `LIST`- `LONGVARCHAR`- `NUMERIC`- `STATUS`- `TIME`- `TIMESTAMP`- `VARCHAR` (默认)
# 详解
此关键字指定将此类投影到客户端技术时使用的客户端数据类型。每个数据类型类都必须指定一个客户端数据类型。
# 对子类的影响
这个关键字是从主超类继承的。子类可以覆盖关键字的值。
# 默认
默认的客户端数据类型是`VARCHAR`。
# <center> 第十四章 类关键字 - ClientName
能够重写此类的客户端投影中使用的默认类名。
# 用法
要在将类投影到客户端时覆盖类的默认名称,请使用以下语法:
```javaClass MyApp.MyClass [ ClientName = clientclassname ] { //class members }```
其中clientclassname是用作客户端名称的不带引号的字符串,而不是类名。
# 详解
该关键字允许在类被投影到客户端时为其定义一个替代名称(例如当使用InterSystems IRIS Java绑定时)
# 对子类的影响
此关键字不是继承的。
# 默认
如果省略此关键字,实际的类名将在客户端上使用。
# <center> 第十五章 类关键字 - CompileAfter
指定此类应在其他(指定的)类之后编译。
# 用法
要指示类编译器应该在其他类之后编译此类,请使用以下语法:
```javaClass MyApp.MyClass [ CompileAfter = classlist ] { //class members }```
其中`classlist`是下列之一:
- 类名。例如:- ```java[ CompileAfter = MyApp.Class1 ]```- 用逗号分隔的类名列表,用括号括起来。例如:
```java[ CompileAfter = (MyApp.Class1,MyApp.Class2,MyApp.Class3) ]```
# 详解
此关键字指定类编译器应该在编译指定的类后编译此类。
通常,当类之间存在编译器无法检测到的依赖关系,以致必须一个接一个地编译时,会使用此关键字。
此关键字仅影响编译顺序,不影响运行时行为。
**注意:`CompileAfter`关键字不能确保在编译这个类之前指定的类是可运行的。**
此外,`CompileAfter`关键字只影响与`System`关键字具有公共值的类。
# 对子类的影响
这个关键字继承自所有超类。如果子类为关键字指定了一个值,该值指定了在子类可以被编译之前必须被编译的附加类。
# 默认
默认情况下,不指定该关键字。
# <center> 第十六章 类关键字 - DdlAllowed
指定`DDL`语句是否可用于更改或删除类定义。仅适用于持久类。
# 用法
要通过`DDL`修改类,请使用以下语法:
```Class MyApp.Person Extends %Persistent [ DdlAllowed ] { //class members }```否则,省略此关键字或使用以下语法:
```Class MyApp.Person Extends %Persistent [ Not DdlAllowed ] { //class members }```
# 详情
此关键字指定是否可以使用`DDL`语句(如删除表、更改表、删除索引等)来更改或删除类定义。
通常,不希望让SQL用户使用`DDL`语句修改类。
# 对子类的影响
此关键字不是继承的。
# 默认
如果省略这个关键字,`DDL`语句就不能用来影响类定义。
# 注意
如果通过执行`DDL CREATE TABLE`语句来创建一个类,那么对于该类,`DdlAllowed`关键字最初将被设置为`true`。
文章
Michael Lei · 八月 20, 2021
这是一个IRIS 2020.2上的代码示例,并非InterSystems 官方支持!
本demo基于原始类描述 is based on the raw class descriptions.使用的数据类是Address, Person, Employee, Company如果要做更有吸引力的 demo, 可以添加 JSONtoString by ID的方法
用ZPM安装后从终端启动:After installation with ZPM just run from Terminal
USER>do ##class(rcc.ONAPI.demo).Run()
Adjust Parameters
host[127.0.0.1]:
port[51773]:
namespace[USER]:
user[_SYSTEM]:
pwd[SYS]:
timeout[5]:
****** connected ********
下一步, 你会得到一系列可能的Demo动作。you get a list of possible demo actions.没有输入就意味着没有动作No input means no action.菜单会一直循环直到退出The menu loops until you exit.
Populate Person by:100
100
Populate Company by:10
10
Populate Employee by:50
50
Show Person by ID:3
{"Name":"Rogers,Norbert V.","SSN":"990-11-9806","DOB":"1962-04-23","Home":{"Street":"867 Oak Street","City":"Denver","State":"NH","Zip":"64647"},"Office":{"Street":"3309 Oak Court","City":"Denver","State":"NY","Zip":"76436"},"FavoriteColors":["Green","Green"],"Age":58}
Show Company by ID:3
{"Name":"CompuComp Corp.","Mission":"Specializing in the development and manufacturing of open-source object-oriented models for additive manufacturing.","TaxID":"Y8155","Revenue":819493934,"Employees":[{"Name":"Ahmed,Sophia P.","SSN":"936-73-5161","DOB":"1933-08-08","Home":{"Street":"8758 Elm Street","City":"Fargo","State":"OH","Zip":"40652"},"Office":{"Street":"1578 Maple Street","City":"Larchmont","State":"IA","Zip":"89021"},"Spouse":{"Name":"Olsen,William A.","SSN":"912-52-4809","DOB":"2010-01-26","Home":{"Street":"2933 Main Street","City":"Bensonhurst","State":"WA","Zip":"51960"},"Office":{"Street":"4994 Ash Street","City":"Gansevoort","State":"OR","Zip":"89750"},"Spouse":{"Name":"Adam,Brian D.","SSN":"799-82-3083","DOB":"2005-11-04","Home":{"Street":"1264 Oak Avenue","City":"Chicago","State":"WV","Zip":"34943"},"Office":{"Street":"7443 Second Avenue","City":"Zanesville","State":"CO","Zip":"30478"},"Spouse":{"Name":"Cooke,Diane C.","SSN":"754-49-5729","DOB":"1984-01-12","Home":{"Street":"9624 Maple Place","City":"Albany","State":"MS","Zip":"60948"},"Office":{"Street":"4711 Second Place","City":"Youngstown","State":"VA","Zip":"31250"},"Age":36},"FavoriteColors":["Orange"],"Age":14},"FavoriteColors":["Green","Green"],"Age":10,"Title":"Senior Systems Engineer","Salary":61511},"Age":87,"Title":"Product Manager","Salary":59127},{"Name":"Ng,Elmo S.","SSN":"201-15-3259","DOB":"1954-10-14","Home":{"Street":"2437 Main Avenue","City":"Xavier","State":"KY","Zip":"10156"},"Office":{"Street":"2770 Oak Drive","City":"Tampa","State":"OH","Zip":"18459"},"Spouse":{"Name":"Eastman,Linda M.","SSN":"110-41-1818","DOB":"1980-03-19","Home":{"Street":"5309 Ash Drive","City":"Xavier","State":"RI","Zip":"36964"},"Office":{"Street":"4288 Washington Place","City":"Xavier","State":"HI","Zip":"41889"},"Spouse":{"Name":"Huff,Dave C.","SSN":"559-32-3838","DOB":"1973-05-05","Home":{"Street":"6216 First Avenue","City":"Tampa","State":"WA","Zip":"68628"},"Office":{"Street":"2896 Clinton Drive","City":"Elmhurst","State":"UT","Zip":"97796"},"FavoriteColors":["Purple"],"Age":47},"FavoriteColors":["Purple","Blue"],"Age":40},"FavoriteColors":["Red","Purple"],"Age":65,"Title":"Laboratory Marketing Manager","Salary":35888}]}
Show Employee by ID:103
{"Name":"Faust,Buzz H.","SSN":"979-41-6347","DOB":"1938-02-07","Home":{"Street":"6231 Madison Avenue","City":"Gansevoort","State":"TX","Zip":"49085"},"Office":{"Street":"6402 Main Street","City":"Elmhurst","State":"RI","Zip":"82976"},"Spouse":{"Name":"Joyce,Chad C.","SSN":"199-86-8085","DOB":"1974-11-17","Home":{"Street":"6229 Main Street","City":"Reston","State":"FL","Zip":"16922"},"Office":{"Street":"8509 Elm Blvd","City":"Bensonhurst","State":"HI","Zip":"90665"},"Spouse":{"Name":"Cooke,Diane C.","SSN":"754-49-5729","DOB":"1984-01-12","Home":{"Street":"9624 Maple Place","City":"Albany","State":"MS","Zip":"60948"},"Office":{"Street":"4711 Second Place","City":"Youngstown","State":"VA","Zip":"31250"},"Age":36},"FavoriteColors":["Purple"],"Age":45},"Age":82,"Title":"Global Administrator","Salary":13813}
Show Global PersonD by ID:4
$Data()=1
Value=$lb("","Eastman,Mary C.","887-18-3730",44711,$lb("3889 Ash Blvd","Washington","TX",67862),$lb("5709 Oak Blvd","Chicago","IL",30845),"","")
Index list for Person & Employee (n,y):y
$Employee
$Person
NameIDX
SSNKey
ZipCode
Exit Demo (n,y,*):
Populate Person by:
Populate Company by:
Populate Employee by:
Show Person by ID:
Show Company by ID:
Show Employee by ID:104
{"Name":"Novello,Emily I.","SSN":"411-35-4234","DOB":"1943-01-07","Home":{"Street":"3353 Washington Court","City":"Hialeah","State":"MT","Zip":"22403"},"Office":{"Street":"9743 Clinton Blvd","City":"Xavier","State":"OH","Zip":"89038"},"Spouse":{"Name":"Goldman,Usha T.","SSN":"465-59-4053","DOB":"1987-07-16","Home":{"Street":"2578 Second Blvd","City":"Gansevoort","State":"FL","Zip":"77552"},"Office":{"Street":"6986 Main Street","City":"Elmhurst","State":"VT","Zip":"48713"},"Spouse":{"Name":"Rogers,Norbert V.","SSN":"990-11-9806","DOB":"1962-04-23","Home":{"Street":"867 Oak Street","City":"Denver","State":"NH","Zip":"64647"},"Office":{"Street":"3309 Oak Court","City":"Denver","State":"NY","Zip":"76436"},"FavoriteColors":["Green","Green"],"Age":58},"Age":33},"FavoriteColors":["Green","White"],"Age":77,"Title":"Senior Product Specialist","Salary":96469}
Show Global PersonD by ID:
Index list for Person & Employee (n,y):
Exit Demo (n,y,*):y
****** done ********
USER>
文章
Nicky Zhu · 一月 8, 2021
我打算基于实例中的数据实现业务智能。 怎样才是设置数据库和环境来使用 DeepSee 的最佳方法呢?

本教程通过 3 个 DeepSee 架构示例来解决此问题。 首先,我们从基本架构模型开始,并重点说明其局限性。 对于复杂程度中等的业务智能应用,建议使用下一个模型,对于大多数用例而言,该模型应该足矣。 在本教程的最后,我们将说明如何增强架构的灵活性以管理高级实现。
本教程中的每个示例都介绍了新的数据库和全局映射,并讨论了为何以及何时设置它们。 在构建架构时,则重点说明更灵活的示例提供的好处。
开始前
主服务器和分析服务器
为了使数据高度可用,InterSystems 通常建议使用镜像或映射,并将 DeepSee 实现基于镜像/映射服务器。 承载数据原始副本的机器称为“主服务器”,而承载数据副本和业务智能应用程序的计算机通常称为“分析服务器”(有时称为“报告服务器”)。
拥有主服务器和分析服务器至关重要,主要原因是避免任一台服务器出现性能问题。 请查阅有关推荐架构的文档。
数据和应用程序代码
通常,将源数据和代码存储在同一数据库中仅对小型应用程序有效。 对于更大型的应用程序,建议将源数据和代码存储在两个专用数据库中,这样您就可以与运行 DeepSee 的所有命名空间共享代码,同时保持数据分离。 源数据的数据库应从生产服务器镜像。 该数据库可以为只读,也可为读写。 建议为此数据库持续启用日志功能。
源类和自定义应用程序应存储在生产和分析服务器上的专用数据库中。 请注意,这两个用于源代码的数据库不需要同步,甚至不需要运行相同的 Caché 版本。 只要定期将代码备份到其他地方,通常就不需要日志。
在本教程中,我们采用以下配置。 分析服务器上的 APP 命名空间有 APP-DATA 和 APP-CODE 作为默认数据库。 APP-DATA 数据库可以访问主服务器上的源数据数据库中的数据(源表类及其事实数据)。 APP-CODE 数据库存储 Caché 代码(.cls 和 .INT 文件)以及其他自定义代码。 数据和代码的这种分离是一种典型的架构,这允许用户,例如,有效地部署 DeepSee 代码和自定义应用程序。
在不同的命名空间上运行 DeepSee
使用 DeepSee 的业务智能实现通常在不同的命名空间中运行。 在本文中,我们将说明如何设置单个的 APP 命名空间,但是相同的过程适用于运行业务智能应用程序的所有名称空间。
文档
建议熟悉文档页面执行初始设置。 该页面的内容包括:设置 Web 应用程序,如何将 DeepSee 全局变量放置在单独的数据库中,以及 Deepeep 全局变量的替代映射列表。
* * *
在本系列的第二部分中,我们将阐述基本架构模型的实现
文章
姚 鑫 · 二月 5
# 第十五章 K - L 开头的术语
### 日志记录 (journaling)
**系统**
一种功能,系统管理员可以选择启用,导致 `IRIS` 在日志文件中记录所有或选定全局的更改。如果发生系统故障,可以将这些更改向前滚动。也就是说,在恢复期间,可以将整个事务重新应用到数据库。另请参见写入镜像日志记录 (`Write Image Journaling`)。
# 以 K 开头的术语
### 密钥分发中心 (`KDC`)
**系统**
密钥分发中心(`Key Distribution Center,KDC`)是 `Kerberos` 安装的一部分,是确保所有参与方正确认证的中央 `Kerberos` 服务器。具体来说,`KDC` 是可信第三方 `Kerberos` 服务器的一部分,负责生成构成票据授予票据(`TGT`)和服务票据(`Service Ticket`)基础的密钥。在 `Windows` 系统中,密钥分发中心是 `Windows` 域控制器(`Domain Controller,DC`)的一部分,有时也称为该名称。这两个缩写的相似性纯属巧合。
### Kerberos
**系统**
`Kerberos` 是由麻省理工学院(`MIT`)的 `Athena` 项目开发的可信第三方认证系统。它通过建立一个认证信息数据库,允许对用户或应用程序(统称为主体,`principals`)进行认证。该数据库是安全的(因此是可信的),并且与执行认证的任何两个主体分离(这就是为什么它是第三方系统)。`Kerberos` 设计用于不一定安全的网络环境,如互联网。自 `1980` 年代末以来,它已在大型商业和教育机构中广泛使用。
### 键(唯一索引) (key (unique index))
**对象(Objects)**
键是唯一索引的另一种名称。
### 键(加密) (key (encryption))
**系统**
用于加密或解密数据的一个大数,与加密算法配合使用。
### 密钥加密密钥 (key-encryption key)
**系统**
在`IRIS` 数据库加密中,涉及的第二个密钥。第一个密钥用于加密数据库,而密钥加密密钥——第二个密钥——用于加密(因此保护)第一个密钥。当数据库加密密钥被激活时,它会使用密钥加密密钥进行解密并加载到内存中以供使用。
### 关键字(类定义) (keyword (class definition))
**对象(Objects)**
关键字在类定义中定义了一个特定的特性。也称为类关键字。
### 关键字(系统元素) (keyword (system element))
**系统**
关键字也可能指`IRIS` 系统的一部分,如函数名称或运算符。
# 以 L 开头的术语
### 语言配置 (language configuration)
**系统**
一组四个表:字符集、排序序列、`$X/$Y` 动作表和模式匹配;定义国家语言支持的设备无关方面。它是设备相关国家语言支持特性的对应部分,即输入/输出转换。
### 许可证 (license)
**系统**
`InterSystems` 与其客户之间的协议,定义了可供客户使用的`IRIS` 软件组件及每个组件可用的用户数量。客户必须持有许可证才能运行 `IRIS`。许可证信息通过产品激活密钥分发,并存储在系统上的名为 `iris.key` 的文件中。
### 列表 (list)
**对象(Objects)**
一种有序的集合,使用槽号访问数据。每个列表在 `SQL` 中被投射为单个列表字段。
### 区域设置 (locale)
**系统**
指定用户语言、国家及任何其他特殊变体偏好的参数。区域设置指定用于数据输入、输出和处理的用户可见惯例,如数字和日期的表示方式,以及星期和月份的名称。
文章
Michael Lei · 五月 12, 2021
关键字:IRIS, IntegratedML, 机器学习, Covid-19, Kaggle
## 目的
最近,我注意到一个用于预测 Covid-19 患者是否将转入 ICU 的 [Kaggle 数据集](https://www.kaggle.com/S%C3%ADrio-Libanes/covid19/kernels)。 它是一个包含 1925 条病患记录的电子表格,其中有 231 列生命体征和观察结果,最后一列“ICU”为 1(表示是)或 0(表示否)。 任务是根据已知数据预测患者是否将转入 ICU。
这个数据集看起来是所谓的“传统 ML”任务的一个好例子。数据看上去数量合适,质量也相对合适。它可能更适合在 [IntegratedML 演示](https://github.com/intersystems-community/integratedml-demo-template)套件上直接应用,那么,基于普通 ML 管道与可能的 IntegratedML 方法进行快速测试,最简单的方法是什么?
## 范围
我们将简要地运行一些常规 ML 步骤,如:
* 数据 EDA
* 特征选择
* 模型选择
* 通过网格搜索调整模型参数
与
* 通过 SQL 实现的整合 ML 方法。
它通过 Docker-compose 等方式运行于 AWS Ubuntu 16.04 服务器。
## 环境
我们将重复使用 [integredML-demo-template](https://openexchange.intersystems.com/package/integratedml-demo-template) 的 Docker 环境:

以下 notebook 文件在“tf2jupyter”上运行,带有 IntegratedML 的 IRIS 在“irismlsrv”上运行。 Docker-compose 在 AWS Ubuntu 16.04 上运行。
## 数据和任务
该数据集包含收集自 385 名患者的 1925 条记录,每名患者正好 5 条记录。 它共有 231 个列,其中有一列“ICU”是我们的训练和预测目标,其他 230 列都以某种方式用作输入。 ICU 具有二进制值 1 或 0。 除了 2 列看上去是分类字符串(在数据框架中显示为“对象”)外,其他所有列都是数值。
import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score, roc_curve
import seaborn as sns
sns.set(style="whitegrid")
import os
for dirname, _, filenames in os.walk('./input'):
for filename in filenames:
print(os.path.join(dirname, filename))
./input/datasets_605991_1272346_Kaggle_Sirio_Libanes_ICU_Prediction.xlsx
df = pd.read_excel("./input/datasets_605991_1272346_Kaggle_Sirio_Libanes_ICU_Prediction.xlsx")
df
PATIENT_VISIT_IDENTIFIER
AGE_ABOVE65
AGE_PERCENTIL
GENDER
DISEASE GROUPING 1
DISEASE GROUPING 2
DISEASE GROUPING 3
DISEASE GROUPING 4
DISEASE GROUPING 5
DISEASE GROUPING 6
...
TEMPERATURE_DIFF
OXYGEN_SATURATION_DIFF
BLOODPRESSURE_DIASTOLIC_DIFF_REL
BLOODPRESSURE_SISTOLIC_DIFF_REL
HEART_RATE_DIFF_REL
RESPIRATORY_RATE_DIFF_REL
TEMPERATURE_DIFF_REL
OXYGEN_SATURATION_DIFF_REL
WINDOW
ICU
1
60th
0.0
0.0
0.0
0.0
1.0
1.0
...
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
0-2
1
1
60th
0.0
0.0
0.0
0.0
1.0
1.0
...
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
2-4
2
1
60th
0.0
0.0
0.0
0.0
1.0
1.0
...
NaN
NaN
NaN
NaN
NaN
NaN
NaN
NaN
4-6
3
1
60th
0.0
0.0
0.0
0.0
1.0
1.0
...
-1.000000
-1.000000
NaN
NaN
NaN
NaN
-1.000000
-1.000000
6-12
4
1
60th
0.0
0.0
0.0
0.0
1.0
1.0
...
-0.238095
-0.818182
-0.389967
0.407558
-0.230462
0.096774
-0.242282
-0.814433
ABOVE_12
1
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
...
1920
384
50th
1
0.0
0.0
0.0
0.0
0.0
0.0
...
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
0-2
1921
384
50th
1
0.0
0.0
0.0
0.0
0.0
0.0
...
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
2-4
1922
384
50th
1
0.0
0.0
0.0
0.0
0.0
0.0
...
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
4-6
1923
384
50th
1
0.0
0.0
0.0
0.0
0.0
0.0
...
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
-1.000000
6-12
1924
384
50th
1
0.0
0.0
1.0
0.0
0.0
0.0
...
-0.547619
-0.838384
-0.701863
-0.585967
-0.763868
-0.612903
-0.551337
-0.835052
ABOVE_12
1925 行 × 231 列
df.dtypes
PATIENT_VISIT_IDENTIFIER int64
AGE_ABOVE65 int64
AGE_PERCENTIL object
GENDER int64
DISEASE GROUPING 1 float64
...
RESPIRATORY_RATE_DIFF_REL float64
TEMPERATURE_DIFF_REL float64
OXYGEN_SATURATION_DIFF_REL float64
WINDOW object
ICU int64
Length: 231, dtype: object
当然,设计此问题及其方法有几个选择。 首先,第一个显而易见的选择是,这可以是一个基本的“二元分类”问题。 我们可以将全部 1925 条记录都视为“无状态”个体记录,不管它们是否来自同一患者。 如果我们将 ICU 和其他值都当作数值来处理,这也可以是一个“回归”问题。
当然还有其他可能的方法。 例如,我们可以有一个更深层的视角,即数据集有 385 个不同的短“时间序列”集,每个患者一个。 我们可以将整个数据集分解成 385 个单独的训练集/验证集/测试集,我们是否可以尝试 CNN 或 LSTM 等深度学习模型来捕获每个患者对应的每个集合中隐藏的“症状发展阶段或模式”? 可以的。 这样做的话,我们还可以通过各种方式应用一些数据增强,来丰富测试数据。 但那就是另一个话题了,不在本帖的讨论范围内。
在本帖中,我们将只测试所谓的“传统 ML”方法与 IntegratedML(一种 AutoML)方法的快速运行。
## “传统”ML 方法?
与大多数现实案例相比,这是一个相对标准的数据集,除了缺少一些值,所以我们可以跳过特征工程部分,直接使用各个列作为特征。 那么,我们直接进入特征选择。
### **插补缺失数据**
首先,确保所有缺失值都通过简单的插补来填充:
df_cat = df.select_dtypes(include=['object'])
df_numeric = df.select_dtypes(exclude=['object'])
imp = SimpleImputer(missing_values=np.nan, strategy='mean')
idf = pd.DataFrame(imp.fit_transform(df_numeric))
idf.columns = df_numeric.columns
idf.index = df_numeric.index
idf.isnull().sum()
###
### **特征选择**
我们可以使用数据框架中内置的正态相关函数,来计算每个列的值与 ICU 的相关性。
#### 特征工程 - **相关性** {#featuring-engineering---correlation}
idf.drop(["PATIENT_VISIT_IDENTIFIER"],1)
idf = pd.concat([idf,df_cat ], axis=1)
cor = idf.corr()
cor_target = abs(cor["ICU"])
relevant_features = cor_target[cor_target>0.1] # correlation above 0.1
print(cor.shape, cor_target.shape, relevant_features.shape)
#relevant_features.index
#relevant_features.index.shape
这将列出 88 个特征,它们与 ICU 目标值的相关度大于 0.1。 这些列可以直接用作我们的模型输入
我还运行了其他几个在传统 ML 任务中常用的“特征选择方法”:
#### 特征选择 - **卡方** {#feature-selection---Chi-squared}
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
from sklearn.preprocessing import MinMaxScaler
X_norm = MinMaxScaler().fit_transform(X)
chi_selector = SelectKBest(chi2, k=88)
chi_selector.fit(X_norm, y)
chi_support = chi_selector.get_support()
chi_feature = X.loc[:,chi_support].columns.tolist()
print(str(len(chi_feature)), 'selected features', chi_feature)
88 selected features ['AGE_ABOVE65', 'GENDER', 'DISEASE GROUPING 1', ... ... 'P02_VENOUS_MIN', 'P02_VENOUS_MAX', ... ... RATURE_MAX', 'BLOODPRESSURE_DIASTOLIC_DIFF', ... ... 'TEMPERATURE_DIFF_REL', 'OXYGEN_SATURATION_DIFF_REL']
特征选择 - **皮尔逊相关**
def cor_selector(X, y,num_feats):
cor_list = []
feature_name = X.columns.tolist()
# calculate the correlation with y for each feature
for i in X.columns.tolist():
cor = np.corrcoef(X[i], y)[0, 1]
cor_list.append(cor)
# replace NaN with 0
cor_list = [0 if np.isnan(i) else i for i in cor_list]
# feature name
cor_feature = X.iloc[:,np.argsort(np.abs(cor_list))[-num_feats:]].columns.tolist()
# feature selection? 0 for not select, 1 for select
cor_support = [True if i in cor_feature else False for i in feature_name]
return cor_support, cor_feature
cor_support, cor_feature = cor_selector(X, y, 88)
print(str(len(cor_feature)), 'selected features: ', cor_feature)
88 selected features: ['TEMPERATURE_MEAN', 'BLOODPRESSURE_DIASTOLIC_MAX', ... ... 'RESPIRATORY_RATE_DIFF', 'RESPIRATORY_RATE_MAX']
#### 特征选择 - **回归特征消除 (RFE)** {#feature-selection---Recursive-Feature-Elimination-(RFE)}
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
rfe_selector = RFE(estimator=LogisticRegression(), n_features_to_select=88, step=100, verbose=5)
rfe_selector.fit(X_norm, y)
rfe_support = rfe_selector.get_support()
rfe_feature = X.loc[:,rfe_support].columns.tolist()
print(str(len(rfe_feature)), 'selected features: ', rfe_feature)
Fitting estimator with 127 features.
88 selected features: ['AGE_ABOVE65', 'GENDER', ... ... 'RESPIRATORY_RATE_DIFF_REL', 'TEMPERATURE_DIFF_REL']
特征选择 - **Lasso**
ffrom sklearn.feature_selection import SelectFromModel
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import MinMaxScaler
X_norm = MinMaxScaler().fit_transform(X)
embeded_lr_selector = SelectFromModel(LogisticRegression(penalty="l2"), max_features=88)
embeded_lr_selector.fit(X_norm, y)
embeded_lr_support = embeded_lr_selector.get_support()
embeded_lr_feature = X.loc[:,embeded_lr_support].columns.tolist()
print(str(len(embeded_lr_feature)), 'selected features', embeded_lr_feature)
65 selected features ['AGE_ABOVE65', 'GENDER', ... ... 'RESPIRATORY_RATE_DIFF_REL', 'TEMPERATURE_DIFF_REL']
特征选择 - **RF 基于树**:SelectFromModel
from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier
embeded_rf_selector = SelectFromModel(RandomForestClassifier(n_estimators=100), max_features=227)
embeded_rf_selector.fit(X, y)
embeded_rf_support = embeded_rf_selector.get_support()
embeded_rf_feature = X.loc[:,embeded_rf_support].columns.tolist()
print(str(len(embeded_rf_feature)), 'selected features', embeded_rf_feature)
48 selected features ['AGE_ABOVE65', 'GENDER', ... ... 'TEMPERATURE_DIFF_REL', 'OXYGEN_SATURATION_DIFF_REL']
#### 特征选择 - **LightGBM** 或 **XGBoost** {#feature-selection---LightGBM-or-XGBoost}
from sklearn.feature_selection import SelectFromModel
from lightgbm import LGBMClassifier
lgbc=LGBMClassifier(n_estimators=500, learning_rate=0.05, num_leaves=32, colsample_bytree=0.2,
reg_alpha=3, reg_lambda=1, min_split_gain=0.01, min_child_weight=40)
embeded_lgb_selector = SelectFromModel(lgbc, max_features=128)
embeded_lgb_selector.fit(X, y)
embeded_lgb_support = embeded_lgb_selector.get_support()
embeded_lgb_feature = X.loc[:,embeded_lgb_support].columns.tolist()
print(str(len(embeded_lgb_feature)), 'selected features: ', embeded_lgb_feature)
embeded_lgb_feature.index
56 selected features: ['AGE_ABOVE65', 'GENDER', 'HTN', ... ... 'TEMPERATURE_DIFF_REL', 'OXYGEN_SATURATION_DIFF_REL']
#### 特征选择 - **全部集成** {#feature-selection---Ensemble-them-all}
feature_name = X.columns.tolist()
# put all selection together
feature_selection_df = pd.DataFrame({'Feature':feature_name, 'Pearson':cor_support, 'Chi-2':chi_support, 'RFE':rfe_support, 'Logistics':embeded_lr_support, 'Random Forest':embeded_rf_support, 'LightGBM':embeded_lgb_support})
# count the selected times for each feature
feature_selection_df['Total'] = np.sum(feature_selection_df, axis=1)
# display the top 100
num_feats = 227
feature_selection_df = feature_selection_df.sort_values(['Total','Feature'] , ascending=False)
feature_selection_df.index = range(1, len(feature_selection_df)+1)
feature_selection_df.head(num_feats)
df_selected_columns = feature_selection_df.loc[(feature_selection_df['Total'] > 3)]
df_selected_columns
我们可以列出通过至少 4 种方法选择的特征:
.png)
... ...
.png)
我们当然可以选择这 58 个特征。 同时,经验告诉我们,特征选择并不一定总是“民主投票”;更多时候,它可能特定于域问题,特定于数据,有时还特定于我们稍后将采用的 ML 模型或方法。
特征选择 - **第三方工具**
有广泛使用的行业工具和 AutoML 工具,例如 DataRobot 可以提供出色的自动特征选择:

从上面的 DataRobot 图表中,我们不难看出,各个 RespiratoryRate 和 BloodPressure 值是与 ICU 转入最相关的特征。
特征选择 - **最终选择**
在本例中,我进行了一些快速实验,发现 LightGBM 特征选择的结果实际上更好一点,所以我们只使用这种选择方法。
df_selected_columns = embeded_lgb_feature # better than ensembled selection
dataS = pd.concat([idf[df_selected_columns],idf['ICU'], df_cat['WINDOW']],1)
dataS.ICU.value_counts()
print(dataS.shape)
(1925, 58)
我们可以看到有 58 个特征被选中;不算太少,也不算太多;对于这个特定的单一目标二元分类问题,看起来是合适的数量。
### **数据不平衡**
plt.figure(figsize=(10,5))
count = sns.countplot(x = "ICU",data=data)
count.set_xticklabels(["Not Admitted","Admitted"])
plt.xlabel("ICU Admission")
plt.ylabel("Patient Count")
plt.show()
这说明数据不平衡,只有 26% 的记录转入了 ICU。 这会影响到结果,因此我们可以考虑常规的数据平衡方法,例如 SMOTE 等。
这里,我们可以尝试其他各种 EDA,以相应了解各种数据分布。
### **运行基本 LR 训练**
Kaggle 网站上有一些不错的快速训练 notebook,我们可以根据自己选择的特征列来快速运行。 让我们从快速运行针对训练管道的 LR 分类器开始:
data2 = pd.concat([idf[df_selected_columns],idf['ICU'], df_cat['WINDOW']],1)
data2.AGE_ABOVE65 = data2.AGE_ABOVE65.astype(int)
data2.ICU = data2.ICU.astype(int)
X2 = data2.drop("ICU",1)
y2 = data2.ICU
from sklearn.preprocessing import LabelEncoder
label_encoder = LabelEncoder()
X2.WINDOW = label_encoder.fit_transform(np.array(X2["WINDOW"].astype(str)).reshape((-1,)))
confusion_matrix2 = pd.crosstab(y2_test, y2_hat, rownames=['Actual'], colnames=['Predicted'])
sns.heatmap(confusion_matrix2, annot=True, fmt = 'g', cmap = 'Reds') print("ORIGINAL")
print(classification_report(y_test, y_hat))
print("AUC = ",roc_auc_score(y_test, y_hat),'\n\n')
print("LABEL ENCODING")
print(classification_report(y2_test, y2_hat))
print("AUC = ",roc_auc_score(y2_test, y2_hat))
y2hat_probs = LR.predict_proba(X2_test)
y2hat_probs = y2hat_probs[:, 1] fpr2, tpr2, _ = roc_curve(y2_test, y2hat_probs) plt.figure(figsize=(10,7))
plt.plot([0, 1], [0, 1], 'k--')
plt.plot(fpr, tpr, label="Base")
plt.plot(fpr2,tpr2,label="Label Encoded")
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC curve')
plt.legend(loc="best")
plt.show()
ORIGINAL
precision recall f1-score support
0 0.88 0.94 0.91 171
1 0.76 0.57 0.65 54
accuracy 0.85 225
macro avg 0.82 0.76 0.78 225
weighted avg 0.85 0.85 0.85 225
AUC = 0.7577972709551657
LABEL ENCODING
precision recall f1-score support
0 0.88 0.93 0.90 171
1 0.73 0.59 0.65 54
accuracy 0.85 225
macro avg 0.80 0.76 0.78 225
weighted avg 0.84 0.85 0.84 225
AUC = 0.7612085769980507
看起来它达到了 76% 的 AUC,准确率为 85%,但转入 ICU 的召回率只有 59% - 似乎有太多假负例。 这当然不理想 - 我们不希望错过患者记录的实际 ICU 风险。 因此,以下所有任务都将集中在如何通过降低 FN 来提高召回率的**目标**上,希望总体准确度有所平衡。
在前面的部分中,我们提到了不平衡的数据,所以第一本能通常是对测试集进行 Stratify(分层),然后使用 SMOTE 方法使其成为更平衡的数据集。
#stratify the test data, to make sure Train and Test data have the same ratio of 1:0
X3_train,X3_test,y3_train,y3_test = train_test_split(X2,y2,test_size=225/1925,random_state=42, stratify = y2, shuffle = True) <span> </span>
# train and predict
LR.fit(X3_train,y3_train)
y3_hat = LR.predict(X3_test)
#SMOTE the data to make ICU 1:0 a balanced distribution
from imblearn.over_sampling import SMOTE sm = SMOTE(random_state = 42)
X_train_res, y_train_res = sm.fit_sample(X3_train,y3_train.ravel())
LR.fit(X_train_res, y_train_res)
y_res_hat = LR.predict(X3_test)
#draw confusion matrix etc again
confusion_matrix3 = pd.crosstab(y3_test, y_res_hat, rownames=['Actual'], colnames=['Predicted'])
sns.heatmap(confusion_matrix3, annot=True, fmt = 'g', cmap="YlOrBr")
print("LABEL ENCODING + STRATIFY")
print(classification_report(y3_test, y3_hat))
print("AUC = ",roc_auc_score(y3_test, y3_hat),'\n\n')
print("SMOTE")
print(classification_report(y3_test, y_res_hat))
print("AUC = ",roc_auc_score(y3_test, y_res_hat))
y_res_hat_probs = LR.predict_proba(X3_test)
y_res_hat_probs = y_res_hat_probs[:, 1]
fpr_res, tpr_res, _ = roc_curve(y3_test, y_res_hat_probs) plt.figure(figsize=(10,10))
#And plot the ROC curve as before.
LABEL ENCODING + STRATIFY
precision recall f1-score support
0 0.87 0.99 0.92 165
1 0.95 0.58 0.72 60
accuracy 0.88 225
macro avg 0.91 0.79 0.82 225
weighted avg 0.89 0.88 0.87 225
AUC = 0.7856060606060606
SMOTE
precision recall f1-score support
0 0.91 0.88 0.89 165
1 0.69 0.75 0.72 60
accuracy 0.84 225
macro avg 0.80 0.81 0.81 225
weighted avg 0.85 0.84 0.85 225
AUC = 0.8143939393939393
所以对数据进行 STRATIFY 和 SMOT 处理似乎将召回率从 0.59 提高到 0.75,总体准确率为 0.84。
现在,按照传统 ML 的惯例来说,数据处理已大致完成,我们想知道在这种情况下最佳模型是什么;它们是否可以做得更好,我们能否尝试相对全面的比较?
### **运行各种模型的训练比较**:
让我们评估一些常用的 ML 算法,并生成箱形图形式的比较结果仪表板:
# compare algorithms
from matplotlib import pyplot
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import StratifiedKFold
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
#Import Random Forest Model
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier
# List Algorithms together
models = []
models.append(('LR', <strong>LogisticRegression</strong>(solver='liblinear', multi_class='ovr')))
models.append(('LDA', LinearDiscriminantAnalysis()))
models.append(('KNN', <strong>KNeighborsClassifier</strong>()))
models.append(('CART', <strong>DecisionTreeClassifier</strong>()))
models.append(('NB', <strong>GaussianNB</strong>()))
models.append(('SVM', <strong>SVC</strong>(gamma='auto')))
models.append(('RF', <strong>RandomForestClassifier</strong>(n_estimators=100)))
models.append(('XGB', <strong>XGBClassifier</strong>())) #clf = XGBClassifier()
# evaluate each model in turn
results = []
names = []
for name, model in models:
kfold = StratifiedKFold(n_splits=10, random_state=1)
cv_results = cross_val_score(model, X_train_res, y_train_res, cv=kfold, scoring='f1') ## accuracy, precision,recall
results.append(cv_results)
names.append(name)
print('%s: %f (%f)' % (name, cv_results.mean(), cv_results.std()))
# Compare all model's performance. Question - would like to see a Integrated item on it?
pyplot.figure(4, figsize=(12, 8))
pyplot.boxplot(results, labels=names)
pyplot.title('Algorithm Comparison')
pyplot.show()
LR: 0.805390 (0.021905) LDA: 0.803804 (0.027671) KNN: 0.841824 (0.032945) CART: 0.845596 (0.053828)
NB: 0.622540 (0.060390) SVM: 0.793754 (0.023050) RF: 0.896222 (0.033732) XGB: 0.907529 (0.040693)

上图看起来表明,XGB 分类器和随机森林分类器的 F1 分数好于其他模型。
让我们也比较一下它们在同一组标准化测试数据上的实际测试结果:
import time
from pandas import read_csv
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
from sklearn.svm import SVC
for name, model in models:
print(name + ':\n\r')
start = time.clock()
model.fit(X_train_res, y_train_res)
print("Train time for ", model, " ", time.clock() - start)
predictions = model.predict(X3_test) #(X_validation)
# Evaluate predictions
print(accuracy_score(y3_test, predictions)) # Y_validation
print(confusion_matrix(y3_test, predictions))
print(classification_report(y3_test, predictions))
LR:
Train time for LogisticRegression(multi_class='ovr', solver='liblinear') 0.02814499999999498
0.8444444444444444
[[145 20]
[ 15 45]]
precision recall f1-score support
0 0.91 0.88 0.89 165
1 0.69 0.75 0.72 60
accuracy 0.84 225
macro avg 0.80 0.81 0.81 225
weighted avg 0.85 0.84 0.85 225
LDA:
Train time for LinearDiscriminantAnalysis() 0.2280070000000194
0.8488888888888889
[[147 18]
[ 16 44]]
precision recall f1-score support
0 0.90 0.89 0.90 165
1 0.71 0.73 0.72 60
accuracy 0.85 225
macro avg 0.81 0.81 0.81 225
weighted avg 0.85 0.85 0.85 225
KNN:
Train time for KNeighborsClassifier() 0.13023699999999394
0.8355555555555556
[[145 20]
[ 17 43]]
precision recall f1-score support
0 0.90 0.88 0.89 165
1 0.68 0.72 0.70 60
accuracy 0.84 225
macro avg 0.79 0.80 0.79 225
weighted avg 0.84 0.84 0.84 225
CART:
Train time for DecisionTreeClassifier() 0.32616000000001577
0.8266666666666667
[[147 18]
[ 21 39]]
precision recall f1-score support
0 0.88 0.89 0.88 165
1 0.68 0.65 0.67 60
accuracy 0.83 225
macro avg 0.78 0.77 0.77 225
weighted avg 0.82 0.83 0.83 225
NB:
Train time for GaussianNB() 0.0034229999999979555
0.8355555555555556
[[154 11]
[ 26 34]]
precision recall f1-score support
0 0.86 0.93 0.89 165
1 0.76 0.57 0.65 60
accuracy 0.84 225
macro avg 0.81 0.75 0.77 225
weighted avg 0.83 0.84 0.83 225
SVM:
Train time for SVC(gamma='auto') 0.3596520000000112
0.8977777777777778
[[157 8]
[ 15 45]]
precision recall f1-score support
0 0.91 0.95 0.93 165
1 0.85 0.75 0.80 60
accuracy 0.90 225
macro avg 0.88 0.85 0.86 225
weighted avg 0.90 0.90 0.90 225
RF:
Train time for RandomForestClassifier() 0.50123099999999
0.9066666666666666
[[158 7]
[ 14 46]]
precision recall f1-score support
0 0.92 0.96 0.94 165
1 0.87 0.77 0.81 60
accuracy 0.91 225
macro avg 0.89 0.86 0.88 225
weighted avg 0.91 0.91 0.90 225
XGB:
Train time for XGBClassifier(base_score=0.5, booster='gbtree', colsample_bylevel=1,
colsample_bynode=1, colsample_bytree=1, gamma=0, gpu_id=-1,
importance_type='gain', interaction_constraints='',
learning_rate=0.300000012, max_delta_step=0, max_depth=6,
min_child_weight=1, missing=nan, monotone_constraints='()',
n_estimators=100, n_jobs=0, num_parallel_tree=1, random_state=0,
reg_alpha=0, reg_lambda=1, scale_pos_weight=1, subsample=1,
tree_method='exact', validate_parameters=1, verbosity=None) 1.649520999999993
0.8844444444444445
[[155 10]
[ 16 44]]
precision recall f1-score support
0 0.91 0.94 0.92 165
1 0.81 0.73 0.77 60
accuracy 0.88 225
macro avg 0.86 0.84 0.85 225
weighted avg 0.88 0.88 0.88 225
结果显示,RF 实际上好于 XGB。 这意味着 XGB 可能在某种程度上有一点过度拟合。 RFC 结果也比 LR 略有改善。
### **通过进一步的“通过网格搜索调整参数”运行所选模型**
现在假定我们选择了随机森林分类器作为模型。 我们可以对此模型执行进一步的网格搜索,以查看是否可以进一步提高结果的性能。
记住,在这种情况下,我们的目标仍然是优化召回率,这通过最大程度地减少患者可能遇到的 ICU 风险的假负例来实现,所以下面我们将“recall_score”来重新拟合网格搜索。 再次像往常一样使用 10 折交叉验证,因为上面的测试集总是设置为 2915 条记录中的 12% 左右。
from sklearn.model_selection import GridSearchCV
# Create the parameter grid based on the results of random search
param_grid = {'bootstrap': [True],
'ccp_alpha': [0.0],
'class_weight': [None],
'criterion': ['gini', 'entropy'],
'max_depth': [None],
'max_features': ['auto', 'log2'],
'max_leaf_nodes': [None],
'max_samples': [None],
'min_impurity_decrease': [0.0],
'min_impurity_split': [None],
'min_samples_leaf': [1, 2, 4],
'min_samples_split': [2, 4],
'min_weight_fraction_leaf': [0.0],
'n_estimators': [100, 125],
#'n_jobs': [None],
'oob_score': [False],
'random_state': [None],
#'verbose': 0,
'warm_start': [False]
}
#Fine-tune by confusion matrix
from sklearn.metrics import roc_curve, precision_recall_curve, auc, make_scorer, recall_score, accuracy_score, precision_score, confusion_matrix
scorers = {
'recall_score': make_scorer(recall_score),
'precision_score': make_scorer(precision_score),
'accuracy_score': make_scorer(accuracy_score)
}
# Create a based model
rfc = RandomForestClassifier()
# Instantiate the grid search model
grid_search = GridSearchCV(estimator = rfc, param_grid = param_grid,
scoring=scorers, refit='recall_score',
cv = 10, n_jobs = -1, verbose = 2)
train_features = X_train_res
grid_search.fit(train_features, train_labels)
rf_best_grid = grid_search.best_estimator_
rf_best_grid.fit(train_features, train_labels)
rf_predictions = rf_best_grid.predict(X3_test)
print(accuracy_score(y3_test, rf_predictions))
print(confusion_matrix(y3_test, rf_predictions))
print(classification_report(y3_test, rf_predictions))
0.92
[[ 46 14]
[ 4 161]]
precision recall f1-score support
0 0.92 0.77 0.84 60
1 0.92 0.98 0.95 165
accuracy 0.92 225
macro avg 0.92 0.87 0.89 225
weighted avg 0.92 0.92 0.92 225
结果表明,网格搜索成功地将总体准确度提高了一些,同时保持 FN 不变。
我们同样也绘制 AUC 比较图:
confusion_matrix4 = pd.crosstab(y3_test, rf_predictions, rownames=['Actual'], colnames=['Predicted'])
sns.heatmap(confusion_matrix4, annot=True, fmt = 'g', cmap="YlOrBr")
print("LABEL ENCODING + STRATIFY")
print(classification_report(y3_test, 1-y3_hat))
print("AUC = ",roc_auc_score(y3_test, 1-y3_hat),'\n\n')
print("SMOTE")
print(classification_report(y3_test, 1-y_res_hat))
print("AUC = ",roc_auc_score(y3_test, 1-y_res_hat), '\n\n')
print("SMOTE + LBG Selected Weights + RF Grid Search")
print(classification_report(y3_test, rf_predictions))
print("AUC = ",roc_auc_score(y3_test, rf_predictions), '\n\n\n')
y_res_hat_probs = LR.predict_proba(X3_test)
y_res_hat_probs = y_res_hat_probs[:, 1]
predictions_rf_probs = rf_best_grid.predict_proba(X3_test) #(X_validation)
predictions_rf_probs = predictions_rf_probs[:, 1]
fpr_res, tpr_res, _ = roc_curve(y3_test, 1-y_res_hat_probs)
fpr_rf_res, tpr_rf_res, _ = roc_curve(y3_test, predictions_rf_probs)
plt.figure(figsize=(10,10))
plt.plot([0, 1], [0, 1], 'k--')
plt.plot(fpr, tpr, label="Base")
plt.plot(fpr2,tpr2,label="Label Encoded")
plt.plot(fpr3,tpr3,label="Stratify")
plt.plot(fpr_res,tpr_res,label="SMOTE")
plt.plot(fpr_rf_res,tpr_rf_res,label="SMOTE + RF GRID")
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC curve')
plt.legend(loc="best")
plt.show()
LABEL ENCODING + STRATIFY
precision recall f1-score support
0 0.95 0.58 0.72 60
1 0.87 0.99 0.92 165
accuracy 0.88 225
macro avg 0.91 0.79 0.82 225
weighted avg 0.89 0.88 0.87 225
AUC = 0.7856060606060606
SMOTE
precision recall f1-score support
0 0.69 0.75 0.72 60
1 0.91 0.88 0.89 165
accuracy 0.84 225
macro avg 0.80 0.81 0.81 225
weighted avg 0.85 0.84 0.85 225
AUC = 0.8143939393939394
SMOTE + LBG Selected Weights + RF Grid Search
precision recall f1-score support
0 0.92 0.77 0.84 60
1 0.92 0.98 0.95 165
accuracy 0.92 225
macro avg 0.92 0.87 0.89 225
weighted avg 0.92 0.92 0.92 225
AUC = 0.8712121212121211
结果表明,经过算法比较和进一步的网格搜索后,我们将 AUC 从 78% 提高到 87%,总体准确度为 92%,召回率为 77%。
### **“传统 ML”方法回顾**
那么,这个结果到底如何? 对于使用传统 ML 算法的基本手动处理是可以的。 在 Kaggle 竞争表中表现如何? 好吧,它不会出现在排行榜上。 我通过 DataRobot 当前的 AutoML 服务运行了原始数据集,在对排名前 43 的模型进行比较后,最好的结果是使用模型“具有无人监督学习功能的 XGB 树分类器”实现的相当于大约 90+% 的 AUC(有待使用同类数据进行进一步确认)。 如果真的想在 Kaggle 上具有竞争力,这可能是我们要考虑的底线模型。 我也会将最佳结果与模型的排行列表放在 github 中。 最后,对于特定于医护场所的现实案例,我的感觉是,我们还需要考虑具有一定程度自定义的深度学习方法,正如本贴的“数据和任务”部分所提到的。 当然,在现实情况下,在哪里收集高质量数据列也可能是一个前期问题。
## IntegratedML 方法?
上文说明了所谓的传统 ML 流程,其中通常包括数据 EDA、特征工程、特征选择、模型选择和通过网格搜索进行性能优化等。 这是我目前能想到的最简单的适合此任务的方法,我们甚至还没有触及模型部署和服务管理生命周期 - 我们将在下一个帖子中探讨这些方面,研究如何利用 Flask/FastAPI/IRIS,并将这个基本的 ML 模型部署到 Covid-19 X-Ray 演示服务栈中。
现在,IRIS 有了 IntegratedML,它是一个优雅的 SQL 包装器,包装了 AutoML 的强大选项。 在第二部分中,我们可以研究如何以大为简化的流程来完成上述任务,这样我们就不必再为特征选择、模型选择和性能优化等问题而烦恼,同时可获得等效的 ML 结果来实现商业利益。
到这里,如果再塞入使用相同数据快速运行 integratedML 的内容,本帖可能太长了,无法在 10 分钟内读完,因此我将该内容移至[第二部分](https://community.intersystems.com/post/run-some-covid-19-icu-predictions-ml-vs-integratedml-part-i)。