当前位置: 首页 > 科技观察

深入比较数据科学工具箱:Python和R的C-C++实现

时间:2023-03-17 10:24:13 科技观察

概述几周前,我有幸在Scipy会议上就Civis如何与Python和R一起工作发表了演讲。为什么要在Python会议上谈论R?这是一场Python与R语言的战争吗?不!争论哪种语言更好是浪费时间。在Civis,我们很高兴地使用这两种语言,不仅是为了解决日常工作中的数据科学问题,也是为了编写其他工具。以下是我在SciPy会议上的一些讨论。问题的现状我们在Civis的同事来自非常不同的学术背景。我工作的研发团队由一名物理学家、一名经济学家、两名统计学家和一名土木工程师组成。在Civis,每个人都在数据科学领域做一些不同的事情。有些领域在R中更受欢迎,有些在Python中更受欢迎,有些在Matlab中更受欢迎。在这种情况下只支持一种语言并不是一个明智的选择。迁移到一门新语言可能会花费很多时间,而且无论在学术界或工业界多年的技能回报如何,允许人们使用他们熟悉的工具可以确保每个人的工作效率更高。我们同时使用这两种语言的另一个原因是现有的统计工具和包。在解决数据科学问题时,我们经常会遇到某些管道所需的特定语言。我们的研究管道就是一个很好的例子。确保随机样本代表语料库需要一个称为raking的过程,传统上,Python在社会科学领域还没有普及,因此我们将只使用R。当然,该调查还包括用于QA的全文搜索。从某种程度上说,R在NLP社区中并不是很流行,所以这部分将由Python来完成。分析调查数据只是我们解决Civis问题的一小步。结合许多不同的语言来实现一个工作流是具有挑战性的。数据科学平台帮我们提交一系列任务节点,然后基础设施负责独立调用每个任务节点,没有任何依赖。但这不是一个非常理想的情况。有两个原因。首先,整个任务变得支离破碎。任务的轻微修改通常会导致全局失败。而且,任何在这个分布式系统中工作的人,都只了解本地的情况,不了解。了解大局。其次,这样做效率不高。在两种语言之间切换通常需要将数据以特定格式加载到磁盘(最坏情况下通常是csv)。这不仅解析起来很昂贵,而且还会丢失一些类型信息。解决方案我概括了在Civis中遇到的各种问题,但理想的状态是怎样的呢?最好的解决方案是我们可以无缝切换工具和语言。很多熟悉Python的人都喜欢用Python做数据分析,R也类似。事实证明,这是完全可能的,并且已经有相当多的项目开始作为跨语言工具:TensorFlow、XGBoost和Stan都在Civis得到广泛使用。移植或分发现有工具也是可能的,我们已经成功地分发了glmnetR包。对于其他为读者编写数据科学工具的人来说,他们从一开始就把这些跨语言放在心上。有几种方法可以做到这一点,但我个人最喜欢的是用C编写底层,然后使用各自的Python和RCapi进行一些绑定/包装。Python和R其实都是用C实现的,这是阻力最小的路径。C是一门古老的语言,C语言社区已经发展出一些强大的工具链。晦涩难懂的编译器错误信息已经成为过去,GCC和Clang(最好的编译器)给出了友好的反馈(Clang网站可以看栗子)。还有各种“消毒剂”可以帮助捕获常见错误或未定义的行为,如内存泄漏(llvm文档)。案例下面我们通过一个小示例,用C编写一个函数,并使其可由Python和R调用。代码和幻灯片可在我的GitHub上获得。Python我们将以下Python函数转换为C:deftally(s):total=0forelmins:total+=elmreturntotalC这与在C中实现的函数相同:#includedoubletally(double*s,size_tn){doubletotal=0;for(size_ti=0;i#include"Python.h"#include"tally.h"staticPyObject*tally_(PyObject*self,PyObject*args){PyObject*buf;if(!PyArg_ParseTuple(args,"O",&buf)){returnNULL;}Py_bufferview;intbuf_flags=PyBUF_ANY_CONTIGUOUS|PyBUF_FORMAT;if(PyObject_GetBuffer(buf,&view,buf_flags)==-1){returnNULL;}if(strcmp(view.format,"d")!=0){PyErr_SetString(PyExc_TypeError,"weonlytakefloats:(");PyBuffer_Release(&view);returnNULL;}doubleresult=tally(view.buf,view.shape[0]);PyBuffer_Release(&view);returnPy_BuildValue("d",result);}staticPyMethodDefMethodTable[]={{"tally",&tally_,METH_VARARGS,"Computethesumofanarray"},{NULL,NULL,0,NULL}};staticstructPyModuleDeftally_module={.m_base=PyModuleDef_HEAD_INIT,.m_name="tally_",.m_size=-1,.m_methods=MethodTable};PyMODINIT_FUNCPyInit_tally_(void){returnPyModule_Create(&tally_module);子样板代码在顶部,我们定义了一个函数,它接受一个Python对象,检查它是否是适当类型的数组,调用我们的计数函数,并返回结果。其余代码定义模块,它告诉Python解释器我们的计数函数的名称及其参数的类型。RR的过程很相似,但是更加简洁:#include#include#include#include"tally.h"SEXPtally_(SEXPx_){double*x=REAL(x_);intn=length(x_);SEXPout=PROTECT(allocVector(REALSXP,1));REAL(out)[0]=tally(x,n);UNPROTECT(1);returnout;}staticR_CallMethodDefcallMethods[]={{"tally_",(DL_FUNC)&tally_,1},{NULL,NULL,0}};voidR_init_tallyR(DllInfo*info){R_registerRoutines(info,NULL,callMethods,NULL,NULL);}这里需要的代码量很大reduced,因为R和Python类型系统不同,没有真正的标量类型,所以我们不需要像上面Python示例中那样对用户输入进行相同级别的验证/验证。其余代码大致相同,我们定义了一组可以在R中编译的函数。包装一个真实世界的例子肯定会更复杂,但整个事情并不难。编写跨语言工具时要记住的几点:如果您打算在两者之间共享功能,请不要依赖宿主语言的api(R或Python)代码。使用错误码传递异常提示,不要直接调用exit或用宿主语言处理异常。***宿主语言负责内存分配和重新分配,这意味着您的C/C++代码应跳过预分配内存和输出过程。相信编译器,你也应该认真对待编译器错误和警告。如果代码编译时出现警告,则代码未完成。无论谁“赢得”这场语言战争,Python和R都将留在数据科学界。这意味着工具开发人员不能忽视另一种语言,构建有用的工具需要两种语言都可用。一种简单的方法是用C或C++编写大量代码,然后使用C的API为两种语言提供包装器。