【翻译】R vs Python:硬碰硬的数据分析

Vik Paruchuri 原文链接

我们将在已有的数十篇从主观角度对比Python和R的文章中加入自己的观点,但是这篇文章旨在更客观地看待这两门语言。我们会平行使用Python和R分析一个数据集,展示两种语言在实现相同结果时需要使用什么样的代码。这让我们了解每种语言的优缺点,而不是猜想。在Dataquest,我们教授两种语言,并认为两者在数据科学工具箱中都占据各自的地位。

我们将会分析一个NBA数据集,包含运动员和他们在2013-2014赛季的表现,可以在这里下载这个数据集。我们展示Python和R的代码,同时做出一些解释和讨论。事不宜迟,现在就开始这场硬碰硬的对决吧!

读取CSV文件

R

nba <- read.csv("nba_2013.csv")

Python

import pandas
nba = pandas.read_csv("nba_2013.csv")

上面的代码分别在两种语言中将包含2013-2014赛季NBA球员的数据的nba_2013.csv文件加载为变量nba。Python中实际的唯一不同是需要加载pandas库以使用Dataframe。Dataframe在R和Python中都可用,它是一个二维数组(矩阵),其中每列都可以是不同的数据类型。在完成这一步后,csv文件在两种语言中都加载为dataframe。

统计球员数量

R

print(dim(nba))
[1] 481  31

Python

print(nba.shape)
(481, 31)

两者分别输出球员数量和数据列数量。我们有481行,或者说球员,和31列关于球员的数据。

查看数据的第一行

R

print(head(nba, 1))
      player pos age bref_team_id
1 Quincy Acy  SF  23          TOT
[output truncated]

Python

print(nba.head(1))
       player pos  age bref_team_id
0  Quincy Acy  SF   23          TOT
[output truncated]

它们几乎完全相同。两种语言都打印出数据的第一行,语法也非常类似。Python在这里更面向对象一些,head是dataframe对象的一个方法,而R具有一个单独的head函数。当开始使用这些语言做分析时,这是一个共同的主题,可以看到Python更加面向对象而R更函数化。

计算每个指标的均值

让我们为每个指标计算均值。如你所见,数据列以类似fg(field goals made)和ast(assists)的名称命名。它们都是球员的赛季统计指标。如果想得到指标的完整说明,参阅这里

R

meanNoNA <- function(values){
    mean(values, na.rm=TRUE)
}
sapply(nba, meanNoNA)
player NA
pos NA
age 26.5093555093555
bref_team_id NA
[output truncated]

Python

import numpy
nba_numeric = nba._get_numeric_data()
nba_numeric.apply(numpy,.mean, axis=0)
age             26.509356
g               53.253638
gs              25.571726
[output truncated]

这里有一些明显的分歧。在两种方法中,我们均在dataframe的列上应用了一个函数。在python中,如果我们在非数值列(例如球员姓名)上应用函数,会返回一个错误。要避免这种情况,我们只有在取平均值之前选择数值列。

在R中,对字符串列求均值会得到NA——not available(不可用)。然而,我们在取均值时需要确实忽略NA(因此需要构建我们自己的函数)。否则类似x3p.这样的一些列的均值将会为NA,这一列代表三分球的比例。有些球员没有投出三分球,他们的百分比就是缺失的。如果我们直接使用R中的mean函数,就会得到NA,除非我们指定na.rm=TRUE,在计算均值时忽略缺失值。

绘制成对散点图

一个探索数据的常用方法是查看列与列之间有多相关。我们将会比较ast,fgtrb

R

library(GGally)
ggpairs(nba[, c("ast", "fg", "trb")])

import seaborn as sns
import matplotlib.pyplot as plt
sns.pairplot(nba[["ast", "fg", "trb"]])
plt.show()

我们会得到非常相似的两张图,但是可以看到R的数据科学生态中有许多较小的软件包(GGally是最常用的R绘图包ggplot2的辅助包)和更多的通用可视化软件包。在Python中,matplotlib是主要的绘图包,seaborn是一个广泛用于matplotlib上的图层。Python中的可视化通常只有一种蛀牙哦的方法完成某件事,而R中可能有许多包支持不同的方法(例如,至少有半打绘制成对散点图的包)。

对球员聚类

另一个很好探索数据的方式是生成类别图。这将会显示哪些球员更相似。

R

library(cluster)
set.seed(1)
isGoodCol <- function(col){
   sum(is.na(col)) == 0 && is.numeric(col)
}
goodCols <- sapply(nba, isGoodCol)
clusters <- kmeans(nba[,goodCols], centers=5)
labels <- clusters$cluster

Python

from sklearn.cluster import KMeans
kmeans_model = KMeans(n_clusters=5, random_state=1)
good_columns = nba._get_numeric_data().dropna(axis=1)
kmeans_model.fit(good_columns)
labels = kmeans_model.labels_

为了正确的聚类,我们移除了所有非数值列,以及包含缺失值的列。在R中,我们在每一列上应用一个函数,如果该列包含任何缺失值或不是数值,则删除它。接下来我们使用cluster包实施k-means聚类,在数据中发现5个簇。通过set.seed设置随机种子以使结果可复现。

在Python中,我们使用了主要的Python机器学习包scikit-learn拟合k-means模型并得到类别标签。数据准备的过程和R非常类似,但是用到了get_numeric_datadropna方法。

绘制类别图

我们现在可以按类别绘制球员分布图以发现模式。首先使用PCA将数据降至2维,然后画图,用不同标记或深浅的点标志类别。

nba2d <- prcomp(nba[,goodCols], center=TRUE)
twoColumns <- nba2d$x[,1:2]
clusplot(twoColumns, labels)

Python

from sklearn.decomposition import PCA
pca_2 = PCA(2)
plot_columns = pca_2.fit_transform(good_columns)
plt.scatter(x=plot_columns[:,0], y=plot_columns[:,1], c=labels)
plt.show()

在R中,我们通过聚类库中的函数clusplot函数绘图,使用内建函数pccomp实行PCA。

在Python中,我们使用scikit-learn库中的PCA类,使用matplotlib创建图形。

划分训练集和测试集

如果我们希望进行监督性机器学习,将数据划分为训练集和测试集是一个避免过拟合的好办法。

R

trainRowCount <- floor(0.8 * nrow(nba))
set.seed(1)
trainIndex <- sample(1:nrow(nba), trainRowCount)
train <- nba[trainIndex,]
test <- nba[-trainIndex,]

Python

train = nba.sample(frac=0.8, random_state=1)
test = nba.loc[~nba.index.isin(train.index)]

你能注意到R有更多的数据分析内建函数,例如floorsampleset.seed,这些函数在Python中通过第三方库被调用(math.floorrandom.samplerandom.seed)。在Python中,最新版本的pandas包含一个sample方法,返回对原始dataframe确定比例的随机抽样,这使得代码更加简洁。在R中,有很多包可以使抽样更容易,但是没有一个比使用内置sample函数更简洁。在两个例子中,我们都设置了随机种子以保证结果的可重复性。

一元线性回归

假设我们希望通过球员的得分预测其助攻次数。

R

fit <- lm(ast ~ fg, data=train)
predictions <- predict(fit, test)

Python

from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train[["fg"]], train["ast"])
predictions = lr.predict(test[["fg"]])

Scikit-learn包含一个线性回归模型,我们可以通过它拟合并生成预测。R依赖于内建函数lmpredictpredict根据传递给它拟合模型的不同会表现出不同的行为,它可以被用于各种各样的模型。

计算模型统计量

R

summary(fit)
Call:
lm(formula = ast ~ fg, data = train)

Residuals:
    Min      1Q  Median      3Q     Max 
-228.26  -35.38  -11.45   11.99  559.61
[output truncated]

Python

import statsmodels.formula.api as sm
model = sm.ols(formula='ast ~ fga', data=train)
fitted = model.fit()
print(fitted.summary())
OLS Regression Results
============================
Dep. Variable:                    ast
R-squared:                       0.568
Model:                            OLS
Adj. R-squared:                  0.567
[output truncated]

如果希望得到类似R平方值这样的模型统计量,在Python中需要比R多做一点。在R中,我们可以使用内建summary函数得到模型信息。在Python中,我们需要使用statsmodels包,这个包包含许多统计模型的Python实现。我们得到类似的结果,总体来说在Python中进行统计分析稍有点困难,一些R中存在的统计方法也没有存在于Python。

拟合一个随机森林模型

一元线性回归表现的不错,但是我们怀疑数据中可能存在非线性。因此,我们想要拟合一个随机森林模型。

R

library(randomForest)
predictorColumns <- c("age", "mp", "fg", "trb", "stl", "blk")
rf <- randomForest(train[predictorColumns], train$ast, ntree=100)
predictions <- predict(rf, test[predictorColumns])

Python

from sklearn.ensemble import RandomForestRegressor
predictor_columns = ["age", "mp", "fg", "trb", "stl", "blk"]
rf = RandomForestRegressor(n_estimators=100, min_samples_leaf=3)
rf.fit(train[predictor_columns], train["ast"])
predictions = rf.predict(test[predictor_columns])

这里主要的区别是R需要使用randomForest库实现算法,而Python中的scikit-learn直接内建其中。scikit-learn为许多不同的机器学习算法提供了统一的交互接口,在Python中每种算法通常只有一个主要的实现。而R中有许多包含单个算法较小的包,一般访问的方法并不一致。这导致算法更加的多样化(很多算法有多个实现,还有那些新问世的算法),但是只有一小部分是可用的。

计算误差

现在已经拟合了两个模型,下面让我们计算误差,使用MSE

R

mean((test["ast"] - predictions)^2)
4573.86778567462

Python

from sklearn.metrics import mean_squared_error
mean_squared_error(test["ast"], predictions)
4166.9202475632374

Python中的scikit-learn库包含我们可以使用的各种误差量度。在R中,可能有一些小的第三方库计算MSE,但是两种语言中手动计算它都很容易。误差的细微差异几乎可以肯定是由于参数调整造成的,并没什么关系。

下载一个网页

现在已经有了2013-2014赛季的NBA球员数据,让我们抓取一些额外数据补充它。为了节省时间,在这里看一场NBA总决赛的比分。

R

library(RCurl)
url <- "http://www.basketball-reference.com/boxscores/201506140GSW.html"
page <- getURL(url)
tc <- textConnection(page)
data <- readLines(tc)
close(tc)

Python

import requests
url = "http://www.basketball-reference.com/boxscores/201506140GSW.html"
data = requests.get(url).content

Python中的requests包为所有的请求类型使用统一的API接口,下载网页非常容易。在R中,RCurl提供稍微复杂方法发起请求。两者都把网页下载为字符串类型的数据。注:这在R中的下一步并不是必须,只是为了比较的原因。

抽取球员比分

现在我们已经下载了网页,需要处理它以抽取球员比分。

R

library(rvest)
page <- read_html(url)
table <- html_nodes(page, ".stats_table")[3]
rows <- html_nodes(table, "tr")
cells <- html_nodes(rows, "td a")
teams <- html_text(cells)

extractRow <- function(rows, i){
    if(i == 1){
        return
    }
    row <- rows[i]
    tag <- "td"
    if(i == 2){
        tag <- "th"
    }
    items <- html_nodes(row, tag)
    html_text(items)
}

scrapeData <- function(team){
    teamData <- html_nodes(page, paste("#",team,"_basic", sep=""))
    rows <- html_nodes(teamData, "tr")
    lapply(seq_along(rows), extractRow, rows=rows)
}

data <- lapply(teams, scrapeData)

Python

from bs4 import BeautifulSoup
import re
soup = BeautifulSoup(data, 'html.parser')
box_scores = []
for tag in soup.find_all(id=re.compile("[A-Z]{3,}_basic")):
    rows = []
    for i, row in enumerate(tag.find_all("tr")):
        if i == 0:
            continue
        elif i == 1:
            tag = "th"
        else:
            tag = "td"
        row_data = [item.get_text() for item in row.find_all(tag)]
        rows.append(row_data)
    box_scores.append(rows)

这将创建一个包含两个列表的列表,第一个是CLE的比分,第二个是GSW的比分。两个都有标题,以及每个球员和他们的比赛统计。我们现在不会将其转换为更多的训练数据,但是如果需要把它们加入nbadataframe,转换可以很容易地完成。

R代码比Python更复杂,因为它没有一个方便的方式使用正则表达式选择内容,因此我们不得不做额外的处理以从HTML中得到队伍名称。R也不鼓励使用for循环,支持沿向量应用函数。我们使用lapply做到这一点,但由于需要处理的每一行都因是否是标题而异,需要传递保留项的索引和整个rows列表给函数。

我们使用rvest,一个广泛使用的新R网络抓取包实现抽取数据,注意这里可以直接传递url给rvest,因此上一步在R中并不是必须的。

在Python中,我们使用了BeautifulSoup,一个最常用的web抓取包。它让我们可以在标签间循环,并以一种直接的方式构建列表的列表。

结论

我们已经看到了如何使用R和Python分析一个数据集。还有很多任务没有深入,例如保存和分享分析结果,测试,确保生产就绪,以及构建更多的可视化。我们会在近期继续探讨这些,从而得到更明确的结论。现在,下面是一些能够得到的:

R更加函数化,Python更面向对象

就像我们在lmpredict和其他函数中看到的那样,R用函数完成大部分工作。对比Python中的`LinearRegression类,还有dataframe的sample方法。

R包含更多的数据分析内建功能,Python依赖于第三方软件包。

当我们查看汇总统计量时,在R中可以直接使用summary内建函数,但是Python中必须依靠statsmodels包。dataframe是R内置的结构,而在Python中由pandas包引入。

Python拥有“主要的”数据分析包,R拥有由较小的包组成的更大的生态系统

在Python中,我们可以使用scikit-learn完成线性回归,随机森林和许多其他任务。它提供了一致的API,并很好的维护。在R中,我们有多种多样的包,但是也更加碎片化和不一致(线性回归是内置的lmrandomForest是单独的包,等等)。

总体上R有更多的统计支持

R是作为统计语言被构建的,它也显示了这一点。Python中的statsmodels和其他软件包提供了统计方法的大部分实现,但是R的生态系统要大的多。

Python中完成非统计任务通常更加直接

有了类似BeautifulSoup和request这样良好维护的软件包,Python中的网页抓取远易于R。这种说法也适于我们还未关注的其他任务,例如保存数据库,部署web服务器或运行复杂的工作流。

数据分析工作流在两者之间有许多相似之处

R和Python之间有一些互相启发的地方(pandas的Dataframe受到R中dataframe的影响,rvest包来自BeautifulSoup的启发),两者的生态系统都在不断发展壮大,对两种语言中许多共同的任务来说,语法和实现都是非常相似的。

总结

Dataquest,我们首先教授Python,但是最近也加入了R的课程。我们看到这两种语言是互补的,虽然Python在更多领域更强大,但R是一种高效的语言。它可以作为Python在数据探索和统计等领域的补充,或者你惟一的数据分析工具。正如本篇文章中所显示的,两种语言有许多相似的语法和实现方法,你不能在一个或另一个,或者两者中出错。