1.即将没落的列线图和已经兴起的预测计算器
列线图是目前临床预测模型主要的展现形式之一,也被认为是预测模型在临床应用的重要形式之一,然而随着预测计算器的兴起,列线图的功能将逐渐被预测计算器所取代,原因有二:一、列线图的使用不够简便。首先,列线图的使用过程要先对照列线图最上端的标尺获得每个预测变量的得分,然后将各个得分相加而获得总分,最后以总分对照概率标尺获得患者患病的风险或者个概率。这个查找过程显然是需要一定的时间,如果预测变量少,时间还可以接受,如果增加预测变量的个数,得出结果的时间显然会超出可接受的范围;其次,列线图不能获得精确的概率。概率标尺本身的精确度有限,虽然目前来看,因为模型普遍准确度有限,获得精确概率的意义不大,但是将来随着模型准确度的提升,获得准确的预测概率或许变得有其必要;二、列线图不能展示非线性模型制作的预测模型的结果。列线图是针对线性预测模型,即用逻辑回归或Cox回归算法制作的预测模型。随着机器学习的非线性算法应用到临床预测模型领域,并显示出优于线性模型的预测准确度,未来非线性算法有可能成为临床预测模型的主流算法。
预测计算器完全克服了以上的两个缺点,使用方便且可以展现非线性模型。R语言提供了专门的库来进行预测计算器的设计,通过一定的学习即可掌握,越来越多的论文会提供预测计算器的链接,国外网站也已经有许多的预测计算器供使用。特别是对于机器学习的非线性模型来说,因为其不能被制作成列线图,预测计算器是唯一的接近应用的表现形式。
现在预测模型的原始数据还是极其难得的,在仅依靠论文公布的模型参数而不要求原始数据的情况下,我们将列线图转化预测模型公式再转化为预测计算器,以充分利用已经发表的预测模型,为临床预测模型走进临床提供便利。
2.列线图转化为预测计算器的策略:
概括来说,将列线图转化为预测计算器的策略是,首先根据论文公布的模型参数和列线图推导出预测模型的公式,然后用编程实现概率或者生存率的计算。转化的前提是获得必要的参数,第一个必需的参数是各个预测因素的权重,即β值或者β值的衍生OR值(HR值),是必需的参数之一;另一个必需的参数在逻辑回归算法是“截距”,在Cox回归是“基础生存率”。获得了以上两个参数就可以推导出预测模型的公式,进而计算出概率或者生存率。
发布预测模型的论文一般都会报道预测因子的OR值(逻辑回归)或者HR值(Cox回归),预测模型的公式中需要的是β值,通过公式β=log(OR)即可通过简单的换算而得到β值。接下来,在逻辑回归算法,是获得“截距”,在Cox回归算法,是获得“基础生存率”,这会分为多种情况。情况1:有的论文会公布逻辑回归的“截距”,这是最理想的情况,但是我还没看见哪个论文公布“基础生存率”;情况2:有的论文会举出一个列线图使用的例子,会给出某个患者预测变量的取值以及预测的概率或者生存率,这种情况下可以通过公式倒推出“截距”或者“基础生存率”。因为生存率计算的公式中有三个未知数(见下文),知道其中的两个即可推导出第三个;情况3:只知道OR(RR)值和列线图。可以通过实际测量列线图而得出一个例子而达到情况2的条件,但是由于列线图的概率标尺是不精确的,所以通过这种情况下得到的“截距”或“基础生存率”也不准确,可能需要多次测量取平均值等方法来修正。
在获得了需要的参数后,我们即可用各种编程工具将公式转化为预测计算器。这里以kivy为例(见下文)。
3.列线图转化为预测计算器的步骤:
下面进入转化实际操作的部分,我们分为逻辑回归和cox回归两种情形。
- 逻辑回归情形:
公式:
线性函数的公式:f(x)=w^T x+b
Sigmoid函数的公式:σ(x)=1/(1+ⅇ^(-x) )
逻辑回归函数的公式(概率的计算公式):y=σ(f(x))=σ(w^T x)=1/(1+ⅇ(-wT x+b) )
结合了线性函数和sigmoid函数,其中的b代表截距,w代表预测变量的权重,即论文中的β值;x是预测变量的取值,知道以上参数即可计算出概率值y。
Python代码的实现:
def prob(self):
ls_beta=[np.log(x) for x in self.ls_or]
ls_weight=[a*b for a,b in zip(ls_beta,self.ls_xvar)]
z=sum(ls_weight)+self.intercept
q=1+np.exp(-z)
prob=1/q
return prob
- Cox回归的情形:
公式:生存率计算公式:S(t|X)=[S0 (t)]^exp(βX)
根据以上公式计算出的就是患者的生存概率S( t|X),公式中β是预测因素的权重,X是预测变量的取值,S0 (t)是基础生存概率,与X无关,是一个常数。
Python代码的实现:
def survival_rate(self):
ls_beta=[np.log(x) for x in self.ls_hr]
ls_weight=[a*b for a,b in zip(ls_beta,self.ls_xvar)]
pi=sum(ls_weight)
survival_rate=self.basic_rate**np.exp(pi)
return survival_rate
用Kivy将列线图转化为预测计算器
列线图原型:
转化的界面:
Python代码的实现:
# -*-coding:utf-8-*-
from kivy.app import App
from kivy.lang.builder import Builder
from kivy.uix.screenmanager import ScreenManager, Screen
from kivy.uix.image import Image
from kivy.properties import ObjectProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
import numpy as np
import kivy
from kivy.uix.popup import Popup
from kivy.core.text import LabelBase
import evassistant as ev
from kivy.uix.label import Label
# # 加载字体资源
kivy.resources.resource_add_path("./")
# #通过labelBase
LabelBase.register("Droid","Droid-Sans-Fallback.ttf")
kivy.core.text.Label.register("Droid","Droid-Sans-Fallback.ttf")
Builder.load_string("""
<MenuScreen>:
name:'Menu'
BoxLayout:
orientation:'vertical'
ActionBar:
id:bar
ActionView:
ActionPrevious:
title:'恶性肿瘤'
font_name:"Droid"
markup:True
with_previous:False
app_icon:'logo.png'
ActionButton:
text:"遇病一测,防微杜渐"
font_name:"Droid"
Accordion:
orientation:'vertical'
AccordionItem:
title:"常见问题问答"
collapse:False
GridLayout:
cols:1
canvas:
Color:
rgb:1,1,0.8
Rectangle:
pos:self.pos
size:self.size
ScrollView:
do_scroll_x:False
do_scroll_y:True
Label:
size_hint_y:None
text:root.txt
font_name:"Droid"
color: 0,0.2,0.4,1
markup:True
height:self.texture_size[1]
text_size:self.width, None
line_height:1.2
# padding_x:2
valign: 'center'
AccordionItem:
title:"胃癌、"
GridLayout:
cols:3
pos_hint: {"center_x": .5, "center_y": .5}
row_force_default:True
row_default_height:200
padding_y:10
canvas:
Color:
rgb:0.6,0.8,0.2
Rectangle:
pos:self.pos
size:self.size
Button:
text:'胃癌生存率'
on_press:root.manager.current='screen1'
# Button:
# text:"2型糖尿病脑卒中"
# on_press:root.manager.current='screen2'
# Button:
# text:"2型糖尿病"
# on_press:root.manager.current='screen4'
# Button:
# text:"冠心病"
# on_press:root.manager.current='screen5'
# Button:
# text:"卒中"
# on_press:root.manager.current='screen7'
# Button:
# text:'认知功能障碍'
# on_press:root.manager.current='screen8'
# Button:
# text:'慢阻肺'
# on_press:root.manager.current='screen11'
# AccordionItem:
# title:"围妊娠期"
# GridLayout:
# cols:3
# pos_hint: {"center_x": .5, "center_y": .5}
# row_force_default:True
# row_default_height:200
# padding_y:10
# Button:
# text:'妊娠期糖尿病'
# on_press:root.manager.current='screen3'
# # background_normal:''
# # background_color:(0,0.6,1,1)
# Button:
# text:"妊娠期高血压"
# on_press:root.manager.current='screen6'
# # Button:
# # text:"2型糖尿病眼底病"
# # Button:
# # text:"早老性痴呆"
# AccordionItem:
# title:"COVID-19 冠状病毒"
# GridLayout:
# cols:3
# pos_hint: {"center_x": .5, "center_y": .5}
# row_force_default:True
# row_default_height:200
# padding_y:10
# Button:
# text:'生存率预测'
# on_press:root.manager.current='screen9'
# # background_normal:''
# # background_color:(0,0.6,1,1)
# Button:
# text:"重症预测"
# on_press:root.manager.current='screen10'
# # Button:
# # text:"2型糖尿病眼底病"
# # Button:
# # text:"早老性痴呆"
<screen1>:
name:'screen1'
BoxLayout:
orientation:'vertical'
ActionBar:
id:bar
ActionView:
ActionPrevious:
title:'胃癌术后生存'
font_name:"Droid"
markup:True
app_icon:'logo.png'
with_previous:True
on_press:root.manager.current="menu"
TabbedPanel:
id:tp
do_default_tab:False
canvas.before:
Color:
rgb:0.4,0.4,0.6
Rectangle:
pos:self.pos
size:self.size
TabbedPanelItem:
text:"ID31027946"
font_name:"Droid"
# border:'off'
GridLayout:
cols:1
padding: '10dp'
spacing:'10dp'
canvas.before:
Color:
rgb:0.8,0.8,1
Rectangle:
pos:self.pos
size:self.size
GridLayout:
cols:6
panding:10
DarkLabel:
text:"年龄"
ToggleButton:
text:'≤44岁'
group:'age'
state:'down'
ToggleButton:
id:45to54
text:"45-54岁"
group:'age'
ToggleButton:
id:55to64
text:'55-64岁'
group:'age'
ToggleButton:
id:65to74
text:'65-74岁'
group:'age'
ToggleButton:
id:75more
text:'≥75岁'
group:'age'
GridLayout:
cols:5
DarkLabel:
text:"肿瘤位置"
ToggleButton:
id:low_site
text:'下1/3'
group:'tumor_site'
state:'down'
ToggleButton:
id:middle_site
text:"中1/3"
group:'tumor_site'
ToggleButton:
id:upper_site
text:"上1/3"
group:'tumor_site'
ToggleButton:
id:overlapping
text:'重叠'
group:'tumor_site'
GridLayout:
cols:2
DarkLabel:
text:"尺寸:"+str(tumor_size.value)
Slider:
id:tumor_size
min:0
max:200
step:10
GridLayout:
cols:6
DarkLabel:
text:"浸润深度"
ToggleButton:
id:mucosa
text:'粘膜/粘膜下层'
group:'invasion_depth'
state:'down'
ToggleButton:
id: muscle
text:"肌肉层"
group:'invasion_depth'
ToggleButton:
id:subserosa
text:"浆膜下层"
group:'invasion_depth'
ToggleButton:
id:serosa
text:'浆膜层'
group:'invasion_depth'
ToggleButton:
id:adjacent_organ
text:'临近器官'
group:'invasion_depth'
GridLayout:
cols:2
DarkLabel:
text:'检查淋巴结:'+str(examined_node.value)
Slider:
id:examined_node
min:0
max:110
step:1
GridLayout:
cols:6
DarkLabel:
text:'转移的淋巴结'
ToggleButton:
text:'0'
group:'metastatic_node'
state:'down'
ToggleButton:
id:1to2
text:'1-2个'
group:'metastatic_node'
ToggleButton:
id:3to6
text:'3-6个'
group:'metastatic_node'
ToggleButton:
id:7to15
text:'7-15个'
group:'metastatic_node'
ToggleButton:
id:16more
text:'≥16个'
group:'metastatic_node'
GridLayout:
cols:3
DarkLabel:
text:'手术切缘'
ToggleButton:
text:'阴性'
group:'margin'
state:'down'
ToggleButton:
id:margin
text:'阳性'
group:'margin'
AButton:
text:'计算'
on_state: root.on_press_prob()
pos_hint:{'center_x':.5,'center_y':.5}
size_hint:(0.9,0.9)
Label:
id:prob_label
text:" "
color:(0.4,0.4,0.6,1)
GridLayout:
cols:4
spacing: '10dp'
AButton:
text:'模型信息表'
on_release:app.info_sheet('PMID31027946.png')
pos_hint:{'center_x':.5,'center_y':.5}
AButton:
text:'评价星级:4星'
on_release:app.conclusion(root.txt)
pos_hint:{'center_x':.5,'center_y':.5}
<AButton@Button>:
font_name:"Droid"
background_normal:""
background_color: 0,0.6,1,1
<Label@Label>:
font_name:"Droid"
<DarkLabel@Label>:
font_name:"Droid"
color:(0.4,0.4,0.6,1)
size_hint_x: None
width:220
text_size:self.width, None
<ActionBar@ActionBar>:
canvas:
Color:
rgb:0.4,0.4,0.6
Rectangle:
pos:self.pos
size:self.size
<ToggleButton@ToggleButton>:
font_name:"Droid"
<Slider@Slider>:
value_track:True
value_track_color:[1,0,0,1]
""")
class MenuScreen(Screen):
txt='''
如何使用预测模型?
首先看模型信息表,了解模型的判断或预测的事件和使用了哪些预测变量以及变量的含义(app中有些变量名称会缩减或有些模型会遇到较为复杂或者综合的变量),尽可能地阅读原始文献来了解模型更多的具体内容;然后可以参考‘推荐星级及其简要评书’,来了解作者对模型的评价;最后,输入相关的参数来获得计算结果。
如何看待预测模型计算的结果?
如果某模型计算的结果是两个患者患糖尿病的概率分别为70%和80%”,可以认为两个患者都属于高风险的患者,并不能说一个患者患病的概率比另一个患者的患病概率高10%,也不能说患者一定的患病的。
如何评价预测模型的优劣?
首先,临床预测模型的优劣一般是通过区分度和校准度两方面来进行评价。区分度最主要的参数是C统计量(逻辑回归模型)和C指数(Cox模型),其通俗理解是我们所构建的模型是否可以找出一个点将发生事件和未发生事件的人群区分开来;校准度一般是通过校准度曲线来直观评价,观察实际曲线与理想曲线的贴合程度,贴合表示校准度优良,不贴合表示高估或者低估实际概率,另外,其斜率可以作为校准度的参数指标。校准度的通俗理解是所构建的模型预测的概率与实际概率的符合程度。
其次,临床预测模的另一个要求有一定的外推性,但是要求一个临床预测模型可以全世界通用是不现实的。构建模型的患者人群与模型应用人群之间的相似程度决定了一个模型预测表现的好和坏,所以,国内人群构建的模型在国内人群应用时会最有可能表现出最佳的性能,某地区人群构建的模型在本地区应用时最有可能表现出最佳的性能,而对于国外人群构建的模型则要通过外部验证(采用本地人群进行)来确定其效能。
再次,模型构建所用到的样本量是影响模型稳定性的一个重要参数。样本量越大代表模型学习了更多的临床情况,在应用到新的情景中时,模型更有可能做出准确的预测。
所以,本APP优先选择纳入国内患者数据构建的,样本量较大的,模型评分较高的临床预测模型。
临床预测模型是否有用?
目前,预测模型辅助临床决策的功能还比较薄弱,主要是许多疾病结局的影响因素并不完全明了,且许多模型的制作不合乎规范,但是可以从临床预测模型中看出哪些是临床结局的危险因素以及其对临床结局贡献的大小。
没有找到感兴趣的预测模型?
如果报道预测模型的论文未报道相关的参数,则无法制作APP;机器学习等'非线性'算法构建的模型一般不公布模型文件,无法构建app;作者未关注到相关模型,读者可以进行推荐或者定制app(liuyp2080@163.com)。
'''
class screen1(Screen):
def __init__(self, **kw):
super().__init__(**kw)
self.txt='''
样本量达到要求;
模型指标良好;
中国人群构建和验证;
具有一定价值。
'''
def on_press_prob(self, *args):
if self.ids['45to54'].state=="down":
age_45to54=1
else:
age_45to54=0
if self.ids['55to64'].state=='down':
age_55to65=1
else:
age_55to65=0
if self.ids['65to74'].state == 'down':
age_65to74= 1
else:
age_65to74 = 0
if self.ids['75more'].state=='down':
age_75more =1
else:
age_75more=0
if self.ids['middle_site'].state=='down':
tumor_site_middle= 1
else:
tumor_site_middle= 0
if self.ids['upper_site'].state=='down':
tumor_site_upper=1
else:
tumor_site_upper=0
if self.ids['overlapping'].state=='down':
tumor_site_overlapping=1
else:
tumor_site_overlapping=0
tumor_size=self.ids['tumor_size'].value
if self.ids['muscle'].state=='down':
invasion_depth_muscle=1
else:
invasion_depth_muscle=0
if self.ids['subserosa'].state=='down':
invasion_depth_subserosa=1
else:
invasion_depth_subserosa=0
if self.ids['serosa'].state=='down':
invasion_depth_serosa=1
else:
invasion_depth_serosa=0
if self.ids['adjacent_organ'].state=='down':
invasion_depth_adjacent_organ=1
else:
invasion_depth_adjacent_organ=0
examined_node=self.ids['examined_node'].value
if self.ids['1to2'].state=='down':
metastatic_node_1to2=1
else:
metastatic_node_1to2=0
if self.ids['3to6'].state=='down':
metastatic_node_3to6=1
else:
metastatic_node_3to6=0
if self.ids['7to15'].state=='down':
metastatic_node_7to15=1
else:
metastatic_node_7to15=0
if self.ids['16more'].state=='down':
metastatic_node_16more=1
else:
metastatic_node_16more=0
if self.ids['margin'].state=='down':
margin=1
else:
margin=0
ls_hr = [1.07,1.08,1.34,1.98,
1.135,1.35,1.18,
1.00425,
2.99,4.79,6.6,8.8,
0.97,
1.286,2.08,3.8,7.1,
1.52]
ls_xvar = [age_45to54,age_55to65,age_65to74,age_75more,
tumor_site_middle,tumor_site_upper,tumor_site_overlapping,
tumor_size,
invasion_depth_muscle,invasion_depth_subserosa, invasion_depth_serosa,invasion_depth_adjacent_organ,
examined_node,
metastatic_node_1to2,metastatic_node_3to6,metastatic_node_7to15,metastatic_node_16more,margin]
# ls_left=[0,0,0,0,
# 0,0,0,
# 0,
# 0,0,0,0,
# 110,
# 0,0,0,0,
# 0]
# ls_right=[1,1,1,1,
# 1,1,1,
# 200,
# 1,1,1,1,
# 0,
# 1,1,1,1,
# 1]
ls_xvar_ex=[0,1,0,0,
0,1,0,
45,
0,0,1,0,
40,
0,0,0,1,
1]
# ls_xvar_max=ls_right
# score=ev.logistic.score(ls_or=ls_hr,ls_xvar_ex=ls_xvar_ex,ls_left_value=ls_left,ls_right_value=ls_right)
# print(score)
survival_rate_ex_3=0.2
survival_rate_ex_5=0.1
basic_rate_3=ev.cox.basic_rate(ls_hr=ls_hr,ls_xvar_ex=ls_xvar_ex,survival_rate_ex=survival_rate_ex_3)
basic_rate_5=ev.cox.basic_rate(ls_hr=ls_hr,ls_xvar_ex=ls_xvar_ex,survival_rate_ex=survival_rate_ex_5)
model_3=ev.cox(ls_hr=ls_hr,ls_xvar=ls_xvar,basic_rate=basic_rate_3)
model_5=ev.cox(ls_hr=ls_hr,ls_xvar=ls_xvar,basic_rate=basic_rate_5)
survival_rate_3 = model_3.survival_rate()
survival_rate_5= model_5.survival_rate()
label = self.ids['prob_label']
label.text = "患者3年和5年的生存率为: " + str(round(survival_rate_3, 2) * 100) + '%和'+str(round(survival_rate_5,2)*100)+'%'
class MPDApp(App):
def info_sheet(self,source):
self.the_info_sheet=Popup(title='Infomation Sheet',
content=Image(source=source))
self.the_info_sheet.open()
def conclusion(self,txt):
self.the_conclusion=Popup(title="Brief Comment",
content=Label(text=txt,
markup=True,
line_height=1.2,
# font_size=16
))
self.the_conclusion.open()
def build(self):
sm=ScreenManager()
sm.add_widget(MenuScreen(name='menu'))
sm.add_widget(screen1(name='screen1'))
return sm
MPDApp().run()
4. 结论
通过以上实践,我们已经可以将现存的大部分列线图转化为预测计算器,便利了临床预测模型在临床的使用,从而为预测模型临床实践领域的推广奠定了基础。预测计算器的使用极为方便,增加预测模型的数量也不会造成负担,使得构建模型的重点放在提高模型的准确率上,而不是如何使用方便上,将会改变预测模型的构建策略。最后需要指出的是,还是有的情况下,无法转化成功。比如,有的论文在Cox回归的基础上用到了立方条样分析方法,其公式为何需要进一步探索。
当然,在将列线图转化为预测计算器的实际操作过程中, 还是会遇到这样那样的问题。