2016五月 西安洛阳行记

西安印象

很热闹的城市,超多游人。
吃的东西,还是自己找小巷子更好。
市区或者有名的文保单位明显不及山西的高质量。去了大雁塔50门票+30登塔,顿时发现佛光寺就算收150页不算贵。(啊啊啊,可怜我硬盘数据丢失,两三年的照片都没了)
最后发张图证明来过西安:
大雁塔

洛阳印象

超棒的小城市,特别喜欢当地人生活的小巷子,特别有人气!(当然让我自己去住我是不愿意的)
可为什么大街上都是医院和卖成人用品的店呢?
白马寺和龙门石窟都超赞!
白马寺
龙门石窟

郑州印象

脏乱差。要不是这次河南博物院大修,我是不愿意来第二次的。

多余的话

那些被技术淘汰的事物:旅游地图/景区照相
以前到达一个地方,总会立马买一张当地的地图。是的,对我来说,这就是外部世界的入口。而如今大家早已经用手机地图了,在火车站/汽车站,还是能够遇到不少这样的老奶奶,举着地图,“5元一份”,看到这样的情景总是很纳闷,难道没有子女提醒他们,现在大部分人已经不买这个了吗?
同样还有景区快照。在以前数码相机还没有普及的时候,无可厚非。但是现在还大规模的打出“5分钟快照”的旗号,你们的世界就不更新吗?除非占据了特别好的机位(只有这个点才能拍到全景,而且我占了,旅游者自己拍不到),或者抓住了时机(比如坐过山车时)。

停车场
秦始皇兵马俑附近农家跑到马路上招揽停车的游人,口号是“只要5块,景区10块”。你们其实没有想到,他们不在乎这五块十块钱,他们在乎的是能不能少走两步路,不谢。

MIT:The Analytics Edge 笔记04-决策树和随机森林

MIT课程 15.071x The Analytics Edge 第四单元的学习记录。


Trees

第四单元的主题是决策树和随机森林。

1.理论

CART(classification and regression trees)

决策树

自变量是决策树上的节点(splits)。但是注意,不是每个自变量都有一个节点;也就是说,有的自变量有多个节点(随着取值的不同,导致因变量的结果也不同),有的自变量没有节点(对因变量影响很小)。
因变量是决策树上的叶子/终端(leaves/nodes)。此图上的因变量的取值是0或者1。
在各个节点,根据各个自变量的取值,最终到达叶子节点,也就得到了因变量的取值。
注意,决策树的左边,节点的判断语句总是为True/Yes,右边节点的判断语句总是为False/No。
tree
最左的分支表示,如果 LowerCou=lbr 且 Responde=CRI 且 Petition=CIT,那么因变量的取值为0。
最右的分支表示,如果 LowerCou!=lbr 且 Responde=STA,那么因变量的取值为1。

决策树的大小

minbucket可以理解为,决策树被节点分割后,每个bucket数据的数量。
minbucket越大,分组越少,split越少。
minbucket越小,分组越多,split越多。

Classification tree 和 Regression tree
  • Classification tree analysis is when the predicted outcome is the class to which the data belongs.(简单的讲,预测值是0和1,比如支持还是反对)
  • Regression tree analysis is when the predicted outcome can be considered a real number.(简单的讲,预测值是可变的,比如房价等等)

体现在代码中的话,
如果指定了type = “class”,那么是 Classification tree。
如果没有指定type = “class”,那么是 Regression tree。

Random Forest

随机森林,被设计出来用于提高CART的精度。
和字面意思类似,如果决策树只有一棵树,那么随机森林会创建多个决策树,然后找到效果最好的那一个。
那么它是如何创建多个决策树的呢,有点复杂。
它并不是多次调用rpart(),简单的调整几个参数而已。
每个决策树所用的数据,都只是原数据的随机subset或者说随机子集。
如果训练集被分成1,2,3,4,5 这五个子集,那么第一次可能选取2,4,5,2,1,第二次可能选取3,5,1,5,2。

参数nodesize

类似于minbucket,每个子集的最小数目。它越小,生成的决策树越大。

参数ntree

生成多少个决策树。一般几百个就够了。

好消息是,参数的选取,相比CART而言,对结果的影响没有那么大。

Cross Validation

minbucket应该选取什么样的值,来大道最好效果呢?
我们采用 k-fold cross validation 的方法。

我们将训练集train分成k份,比如 k=5 的时候,
我们先用1,2,3,4来训练,5用来验证;
再用1,2,3,5来训练,4用来验证;
再用1,2,4,5来训练,3用来验证。。。
所以模型中创建了很多决策树。
我们测试每个分割方法下,参数每一个可能的取值,计算这个取值对应的预测精度,绘制曲线。
曲线的X轴是参数的取值,Y轴是预测精度,这样可以很容易找到参数的最佳取值。

CP

像R平方一样,我们也定义了一个概念 cp(complexity parameter) 用来观测效果。
cp越小,决策树越大(over fitting)。

formula
formula
formula + \lambda*s )
formula})
cp越大,分母越小,tree越小。
cp越小,分母越大,tree越大。

2.建模和评估

CART

# Install rpart library
install.packages("rpart")
library(rpart)
install.packages("rpart.plot")
library(rpart.plot)

# CART model
# method="class" 表示我们创建了一个 classification tree
StevensTree = rpart(Reverse ~ Circuit + Issue + Petitioner + Respondent + LowerCourt + Unconst, data = Train, method="class", minbucket=25)

# plot tree
prp(StevensTree)

# Make predictions
# 记得指定 type = "class"
PredictCART = predict(StevensTree, newdata = Test, type = "class")
table(Test$Reverse, PredictCART)

# ROC curve
library(ROCR)

PredictROC = predict(StevensTree, newdata = Test)
# 注意这里没有指定 type = "class"
# 也就是说,学习得到 classification tree 的模型,但是评估使用 regression tree
# 真是天杀的。。。
# 这个PredictROC 有两列
# 第一列是预测y=0的概率
# 第二列是预测y=1的概率
# 如果比较一下 PredictROC 每行的数据,可以发现这两个概率和为1!那是当然!
# 如果拿 PredictROC 和 PredictCART相比
# 如果 PredictROC[n,2]>0.5,那么PredictCART[n]=1。
# 如果 PredictROC[n,2]<0.5,那么PredictCART[n]=0。
# 所以下面我们只使用第二列

pred = prediction(PredictROC[,2], Test$Reverse)
perf = performance(pred, "tpr", "fpr")
plot(perf)

# AUC
as.numeric(performance(pred, "auc")@y.values)

Random Forest

install.packages("randomForest")
library(randomForest)

# Build random forest model
StevensForest = randomForest(Reverse ~ Circuit + Issue + Petitioner + Respondent + LowerCourt + Unconst, data = Train, ntree=200, nodesize=25 )
# Warning message:
# In randomForest.default(m, y, ...) :
#   The response has five or fewer unique values.  Are you sure you want to do regression?

# 如上面的提示消息所示
# randomForest认为因变量的取值很少,不应该用regression
# 但是 random forest 没有 type = "class" 这样的参数
# 所以我们必须确保因变量这一列的取值都是因子
# Convert outcome to factor
Train$Reverse = as.factor(Train$Reverse)
Test$Reverse = as.factor(Test$Reverse)

# Try again
StevensForest = randomForest(Reverse ~ Circuit + Issue + Petitioner + Respondent + LowerCourt + Unconst, data = Train, ntree=200, nodesize=25 )

# Make predictions
PredictForest = predict(StevensForest, newdata = Test)
table(Test$Reverse, PredictForest)

Cross Validation

# Install cross-validation packages
install.packages("caret")
library(caret)
install.packages("e1071")
library(e1071)

# Define cross-validation experiment
numFolds = trainControl( method = "cv", number = 10 )
cpGrid = expand.grid( .cp = seq(0.01,0.5,0.01)) 

# Perform the cross validation
train(Reverse ~ Circuit + Issue + Petitioner + Respondent + LowerCourt + Unconst, data = Train, method = "rpart", trControl = numFolds, tuneGrid = cpGrid )

# Create a new CART model
StevensTreeCV = rpart(Reverse ~ Circuit + Issue + Petitioner + Respondent + LowerCourt + Unconst, data = Train, method="class", cp = 0.18)

# Make predictions
PredictCV = predict(StevensTreeCV, newdata = Test, type = "class")
table(Test$Reverse, PredictCV)

参数cp和loss的使用

# Penalty Matrix
PenaltyMatrix = matrix(c(0,1,2,3,4,2,0,1,2,3,4,2,0,1,2,6,4,2,0,1,8,6,4,2,0), byrow=TRUE, nrow=5)

# CART model
ClaimsTree = rpart(bucket2009 ~ age + alzheimers + arthritis + cancer + copd + depression + diabetes + heart.failure + ihd + kidney + osteoporosis + stroke + bucket2008 + reimbursement2008, data=ClaimsTrain, method="class", cp=0.00005)

prp(ClaimsTree)

# Make predictions
PredictTest = predict(ClaimsTree, newdata = ClaimsTest, type = "class")
# New CART model with loss matrix
ClaimsTree = rpart(bucket2009 ~ age + alzheimers + arthritis + cancer + copd + depression + diabetes + heart.failure + ihd + kidney + osteoporosis + stroke + bucket2008 + reimbursement2008, data=ClaimsTrain, method="class", cp=0.00005, parms=list(loss=PenaltyMatrix))

# Redo predictions and penalty error
PredictTest = predict(ClaimsTree, newdata = ClaimsTest, type = "class")

MIT:The Analytics Edge 笔记03-指数回归

MIT课程 15.071x The Analytics Edge 第三单元的学习记录。


Logistic Regression

第三单元的主题是指数回归。

1.理论

指数回归

指数回归用于因变量y是二进制的情况,也就是说,y的取值只有1或者0。
y=1的概率:
formula=\frac{1}{1+e^{-{(\beta_0 +\beta_1x_1+\beta_2x_2+\ldots+\beta_nx_n+\epsilon)}}})

y=1的概率与y=0的概率的比值:
formula}{P(y=0)})

formula}{1-P(y=1)})

formula

混淆矩阵(confusion matrix)

有阈值t,
如果P(y=1) >=t,则预测y=1。
如果P(y=1) < t,则预测y=0。

对于预测结果,我们得到矩阵

predict y=0 predict y=1
actual y=0 TN (True Nagative) FP (False Positive)
actual y=1 FN (False Nagative) TP (True Positive)

根据矩阵中的值,我们可以计算指数回归的一些指标:

formula
formula
formula

补充概念:
适合率
formula
再现率
formula
F值(F-measure)
formula
F值越高,性能越好

ROC曲线

ROC曲线 (Receiver Operator Characteristic curve)可以指导我们如何选取阈值t。
y轴的指标是 sensitivity,所以也叫 True positive rate。

formula
x轴的指标是 1-specificity,所以也叫 False positive rate。

formula

每取一个阈值t,则计算相对应的 TPR 和 FPR,在坐标里标出这个点,就形成ROC曲线。
ROC Curve

如图所示,

t=0时,我们预测所有的y=1,即TPR=1,FPR=1,对应的坐标是(1,1)   
t=1时,我们预测所有的y=0,即TPR=0,FPR=0,对应的坐标是(0,0)   

这就是曲线的两个端点。

AUC值

AUC(Area Under Curve)被定义为ROC曲线下的面积,显然这个面积的数值不会大于1。又由于ROC曲线一般都处于y=x这条直线的上方,所以AUC的取值范围在0.5和1之间。

2.建立回归模型

# 建立模型
# Top10作为因变量,其他所有的列都作为自变量
SongsLog1 = glm(Top10 ~ ., data=SongsTrain, family=binomial)

# Top10作为因变量,除了loudness以外的所有列都作为自变量
SongsLog2 = glm(Top10 ~ . - loudness, data=SongsTrain, family=binomial)

3.评估

# 预测
testPredict = predict(SongsLog3, newdata=SongsTest, type="response")

# 生成混淆矩阵
table(SongsTest$Top10, testPredict >= 0.45)

# 生成ROC曲线
library(ROCR)
pred = prediction(testPredict, test$violator)
perf = performance(pred, "tpr", "fpr")
plot(perf)

# 加点颜色和坐标点
plot(perf, colorize=TRUE, print.cutoffs.at=seq(0,1,0.1), text.adj=c(-0.2,1.7))

# 计算AUC值
as.numeric(performance(pred, "auc")@y.values)

附录A 分割train和test的方法一

library(caTools)
set.seed(144)

split = sample.split(parole$violator, SplitRatio = 0.7)
train = subset(parole, split == TRUE)
test = subset(parole, split == FALSE)
# 特别注意:每次运行出来的结果是不一样的

也可以这样做:

library(caTools)
set.seed(144)

split = sample(1:nrow(data), size=0.7 * nrow(data))
train = data[split,]
test = data[-split,]

附录B 补充缺失数据

library(mice)
set.seed(144)
vars.for.imputation = setdiff(names(loans), "not.fully.paid")
imputed = complete(mice(loans[vars.for.imputation]))
loans[vars.for.imputation] = imputed

MIT:The Analytics Edge 笔记02-线性回归

MIT课程 15.071x The Analytics Edge 第二单元的学习记录。


Linear Regression

第二单元的主题是线性回归。

1.理论

一元线性回归公式:

formula

其中x是自变量independent variable,y是因变量dependent variable。
beta是相关系数coefficient,epsilon是误差error。

为了判断线性回归的效果,我们有如下检验标准:

1.SSE(sum of squared errors)
注意这里的误差是实际值相对于预测值的。

formula

2.SST (total sum of square)
公式同上。但这里的误差是实际值相对于baseline的。baseline是因变量的平均值。
所以有 0 <= SSE <= SST 。

3.RMSE(root mean square error)

formula

4.R平方

formula

R平方越接近1越好。

多元线性回归公式:

formula

所有数据分析,都要经历 training-test-predict 这三个过程。
在接下来的例子中,我们介绍 建模-评估 这前两个过程。

补充一个relative error的公式:

formula

2.0 事前整理

2.0.1 去除空值

# 如果数据中包含空值
DF = na.omit(DF)

2.0.2 reference level
有些列时字符型的,它们无法进行计算。
如果某列的因子不算多,我们可以把这一列变换成多个可以用于计算的列。

# 假设DF$colr有因子 "Red"4次, "Blue"3次, "Yellow"2次
DF$colr = relevel(DF$colr, "red")

# 效果是,DF$colr 这一列不见了
# 增加了两列 DF$colrBlue 和 DF$colrYellow
# 原先 DF$colr == "Red" 的那些行,它们 colrBlue 和 colrYellow 的值都是0
# 原先 DF$colr == "Blue" 的那些行,它们 colrBlue=1, colrYellow=0
# 原先 DF$colr == "Yellow" 的那些行,它们 colrBlue=0, colrYellow=1

2.建立回归模型

建模使用lm()函数。
DF是保存学习数据的data.frame。

model = lm(y ~ x1 + x2 + ... +xn, data = DF)
# y不要写成 DF$y
# x1也不要写成 DF$x1
# 否则,后面做预测predict()的时候,DFTest代入会报warning

# 除了y列以外所有列
model = lm(y ~ ., data = DF)

# 误差 model$residuals
SSE = sum(model$residuals^2)

随便看个结果吧

> summary(model)

Call:
lm(formula = Price ~ HarvestRain + WinterRain, data = wine)

Residuals:
    Min      1Q  Median      3Q     Max 
-1.0933 -0.3222 -0.1012  0.3871  1.1877 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept)  7.865e+00  6.616e-01  11.888 4.76e-11 ***
HarvestRain -4.971e-03  1.601e-03  -3.105  0.00516 ** 
WinterRain  -9.848e-05  9.007e-04  -0.109  0.91392    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 0.5611 on 22 degrees of freedom
Multiple R-squared:  0.3177,    Adjusted R-squared:  0.2557 
F-statistic: 5.122 on 2 and 22 DF,  p-value: 0.01492

Call表示建模使用的语句。
Residuals表示误差。
Coefficients表示系数,就是公式里面的beta。
Estimate的第一行是常数beta0,第二行是第一个自变量的系数beta1,第三行是第二个自变量的系数beta2,后面类推。
t value越大越好

formula

Pr(>|t|) 和t value相反,越小越好。
最后一列星星越多越好。
三短横下面这行解释了星星的含义。
Multiple R-squared就是R平方,越接近1越准确。

3.评估

对于刚过简历的模型,我们使用测试数据来评估一下准确度。
model就是上文建立的模型。
DFTest是测试数据,它的结构和上文的DF一样。

predict = predict(model, newdata = DFTest)

这个命令的返回值是 DFTest$Price 的预测结果。你可以跟 DFTest$Price 的实际结果相比较,计算SSE、RMSE、R平方等等来衡量对测试数据的预测的准确性。

SSE = sum( (DFTest$Price - predict)^2 )
SST = sum( (DFTest$Price - mean(DF$Price)^2 )
R2 = 1 - SSE/SST

4.Correlation

线性相关性

cor(var1, var2)
# 也可以考察整个DF中,每两列的线性相关性
cor(DF)

返回值是斜率。

建立线性回归模型的时候,应该去掉相关性比较高的列。

补充知识A-棒球统计术语

完全不懂棒球啊,一开始摸不着头脑。

缩写 原文 中文
RS Run Scores 跑分,得分
RA Run Allowed 失分,对手得分
OBP On-Base Percentage 上垒率
OOBP Opponent On-Base Percentage 对手上垒率
SLG Slugging Percentage 长打率,击中率
OSLG Opponent Slugging Percentage 对手长打率
BA Batting Avarage 平均成功率

补充知识B-篮球统计术语

年轻时看NBA,好歹知道一点。

缩写 原文 中文
PTS Points 得分
oppPTS Opponent Points 失分,对手得分
FG Field Goals (success) 进球数
FGA Field Goals Attempted 出手次数
X2P 2 Points 2分球进球数
X2PA 2 Points Attempted 2分球出手次数
X3P 3 Points 3分球进球数
X3PA 3 Points Attempted 3分球出手次数
FT Free Throw 罚球进球数
FTA Free Throw Attempted 发球出手次数
ORB Offensive Rebounds 前场篮板,进攻篮板
DRB Defensive Rebounds 后场篮板,防守篮板
AST Assists 助攻
STL Steals 抢断
BLK Blocks 盖帽
TOV Turnovers 失误

注:X2P列,原始数据列名是2P。由于R不支持数字开头的列名/变量,读取CSV文件的时候,会在原列名2P前加个X,从而变成 X2P。

补充知识C-滞后序列

函数lag,用于生成滞后/偏移序列?

lag(x, k = 1, ...)
# k < 0, previous observations   
# k > 0, future observations
# na.pad=TRUE, add missing values

MIT:The Analytics Edge 笔记01-R语言入门

MIT课程 15.071x The Analytics Edge 第一单元的学习记录。


R语言入门

R语言入门只讲了一些常用的操作。相对于动辄花一本书来讲这些,真是相当简约。但其实足够了,其他操作,需要的时候再查嘛。
数据分析的四要素:data、models、decisions、value

简单使用

帮助:

?func

显示当前的临时变量:

ls()

获取/设置当前目录:

getwd()
setwd()

显示当前文件夹下的文件:

dir()

数据结构

向量概念:

所有的操作都是对向量的每个元素实施的

data.frame:

observation:行
variable   :列,data.frame是按照列存储的
rbind()合并两个data.frame

序列:

自动生成序列seq()

获取数据

读取CSV文件:

DF = read.csv("file_path")
# 返回值是data.frame

查看DF的基本信息:

str(DF)
# DF的结构信息。行和列的数目,列名、列的类型、列的数据举例。

summary(DF)
# 每列的最大值、最小值、中位数、平均数、1/4值、3/4值,以及是否包含空值。

写入CSV文件:

write.csv(DF, "file_path")

从内存中删除变量:

rm(DF)

数据操作

选取一部分数据:

subset( DF, 条件1 & 条件2 | 条件3)

按照列名选取3列:

DF[c(var1, var2, var3)]

选取1,3,5列:

DF[c(1, 3, 5)]

计算平均值和标准差:

mean(DF$var)
sd(DF$var)

返回最大值/最小值的位置(index):

which.max(DF$var)
which.min(DF$var)

返回行数:

nrow(DF)

绘图

直方图,反映一列数据的分布情况:

hist(DF$var, xlim = c(1, 100), breaks = 100)
# xlim 限定范围
# breaks x轴的精确度。注意是针对原始数据的,不是对限定后的

箱型图:

boxplot(DF$var1 ~ DF$var2, xlab = "x-label", ylab = "y-label", main = "title")

点阵图:

plot(......, col = "red")
line(......, col = "blue") # 在原先的基础上再加一条

函数jitter() # 对于有很多重合的点阵图,先用jitter偏移一点,这样看上去效果好很多

其他共通的参数:

col = "red"
type = "line" # 可以指定1,2,3,4,5

聚合

分组:

table(DF$var1)
# var1列中,每种数据的数量的统计
table(DF$var1, DF$var2)
# var1和var2列中,每种数据的数量的交叉统计

分组计算:

tapply(DF$var1, DF$var2, func)
# DF$var1, 原始数据
# DF$var2, 分组依据
# func, 要应用的函数

data.table 教程5-数据拆分和合并

目录:

  1. data.table 介绍
  2. 语义引用
  3. 主键、基于二分法搜索的subset
  4. 二次索引和自动索引
  5. 数据拆分和合并

原文地址:
data.table/wiki/Getting-started


数据拆分和合并

这一讲我们学习reshaping函数 melt 和 dcast 原本的用法,以及从R语言 v1.9.6版以后,函数 melt 和 dcast 新扩展的功能(它们能操作多个列)。


数据

我们在讲解的时候直接加载数据。

介绍

data.table的函数melt 和 dcast 是增强包reshape2里同名函数的扩展。
在这一讲,我们会:

  • 首先,简单看一下原先的函数 melt 和 dcast,它们是如何reshaping一个data.table。
  • 然后,了解一下当前的功能是如何变得冗长而且低效。
  • 最后,学习一下改进之后的函数 melt 和 dcast 如何同时处理多个列。

扩展后的功能符合data.table的设计哲学:运行高效,语法简明。

注意:

从R语言 v1.9.6版以后,你再也不需要加载增强包 reshape2 了,只需要加载 data.table。如果你已经加载了 reshape2 来处理矩阵或者data.frame,那么一定要确保在这之后再加载 data.table。

1.原生的melt/dcast

a) 函数melt

假设我们有下面这样的data.table:

DT = fread("melt_default.csv")
DT
#    family_id age_mother dob_child1 dob_child2 dob_child3
# 1:         1         30 1998-11-26 2000-01-29         NA
# 2:         2         27 1996-06-22         NA         NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21
# 5:         5         29 2000-12-05 2005-02-28         NA
## dob stands for date of birth.

str(DT)
# Classes 'data.table' and 'data.frame':    5 obs. of  5 variables:
#  $ family_id : int  1 2 3 4 5
#  $ age_mother: int  30 27 26 32 29
#  $ dob_child1: chr  "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
#  $ dob_child2: chr  "2000-01-29" NA "2004-04-05" "2009-08-27" ...
#  $ dob_child3: chr  NA NA "2007-09-02" "2012-07-21" ...
#  - attr(*, ".internal.selfref")=<externalptr>

-转化DT,使得每个小孩的出生信息都独占一条数据
我们可以对函数 melt() 指定参数 id.vars 和 measure.vars 来实现

DT.m1 = melt(DT, id.vars = c("family_id", "age_mother"),
        measure.vars = c("dob_child1", "dob_child2", "dob_child3"))
DT.m1
#     family_id age_mother   variable      value
#  1:         1         30 dob_child1 1998-11-26
#  2:         2         27 dob_child1 1996-06-22
#  3:         3         26 dob_child1 2002-07-11
#  4:         4         32 dob_child1 2004-10-10
#  5:         5         29 dob_child1 2000-12-05
#  6:         1         30 dob_child2 2000-01-29
#  7:         2         27 dob_child2         NA
#  8:         3         26 dob_child2 2004-04-05
#  9:         4         32 dob_child2 2009-08-27
# 10:         5         29 dob_child2 2005-02-28
# 11:         1         30 dob_child3         NA
# 12:         2         27 dob_child3         NA
# 13:         3         26 dob_child3 2007-09-02
# 14:         4         32 dob_child3 2012-07-21
# 15:         5         29 dob_child3         NA
str(DT.m1)
# Classes 'data.table' and 'data.frame':    15 obs. of  4 variables:
#  $ family_id : int  1 2 3 4 5 1 2 3 4 5 ...
#  $ age_mother: int  30 27 26 32 29 30 27 26 32 29 ...
#  $ variable  : Factor w/ 3 levels "dob_child1","dob_child2",..: 1 1 1 1 1 2 2 2 2 2 ...
#  $ value     : chr  "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
#  - attr(*, ".internal.selfref")=<externalptr>

说明:

  • 参数 measure.vars 指定了想要拆分(或合并)的列。
  • 我们也可以指定索引而不是列名。
  • 默认的,variable列是 factor(因子)类型的。如果你想返回一个字符型的向量,可以将参数 variable.factor 设为 FALSE。参数 variable.factor 是data.table的函数melt() 里独有的,增强包reshape2 里面没有这个参数。
  • 默认的,转化果的列被自动命名为 variable 和 value。
  • 在结果里,函数melt() 保持了原来列的属性。

-分别将 variable列和 value列重命名为 child 和 dob

DT.m1 = melt(DT, measure.vars = c("dob_child1", "dob_child2", "dob_child3"),
           variable.name = "child", value.name = "dob")
DT.m1
#     family_id age_mother      child        dob
#  1:         1         30 dob_child1 1998-11-26
#  2:         2         27 dob_child1 1996-06-22
#  3:         3         26 dob_child1 2002-07-11
#  4:         4         32 dob_child1 2004-10-10
#  5:         5         29 dob_child1 2000-12-05
#  6:         1         30 dob_child2 2000-01-29
#  7:         2         27 dob_child2         NA
#  8:         3         26 dob_child2 2004-04-05
#  9:         4         32 dob_child2 2009-08-27
# 10:         5         29 dob_child2 2005-02-28
# 11:         1         30 dob_child3         NA
# 12:         2         27 dob_child3         NA
# 13:         3         26 dob_child3 2007-09-02
# 14:         4         32 dob_child3 2012-07-21
# 15:         5         29 dob_child3         NA

说明:

  • 默认的,参数id.vars 或 measure.vars 中的一个省略了,剩余的列自动被赋值给省略的那个参数。
  • 如果参数id.vars 和 measure.vars 都没有指定,所有不是numberic/integer/logical的列都会被赋值给 id.vars。另外,系统还会输出一条警告消息,提示那些列被认为是 id.vars。

b) 函数cast

在前面一节,我们知道如何分拆数据。这一节,我们学习相反的操作。
-如何将刚刚分拆的 DT.m 还原成 DT
也就是,我们想把每个家庭/母亲的所有小孩,都合并到同一行里。我们可以像下面这样使用函数 dcast()。

dcast(DT.m1, family_id + age_mother ~ child, value.var = "dob")
#    family_id age_mother dob_child1 dob_child2 dob_child3
# 1:         1         30 1998-11-26 2000-01-29         NA
# 2:         2         27 1996-06-22         NA         NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21
# 5:         5         29 2000-12-05 2005-02-28         NA

说明:

  • 函数 dcast() 使用了操作符“~”,左边是作为 id.vars 的列,右边是作为 measure.vars 的列。
  • 参数 value.var 指定了需要被分拆扩张的列。
  • 函数 dcast() 也会在结果中尽量保持原来的属性。

-对于 DT.m,如何知道每个家庭有几个小孩
可以给函数 dcast() 的参数 fun.aggregate 传递一个函数。当操作符“~”不方便指定列名的时候,这个功能特别有用。

dcast(DT.m1, family_id ~ ., fun.agg = function(x) sum(!is.na(x)), value.var = "dob")
#    family_id .
# 1:         1 2
# 2:         2 1
# 3:         3 3
# 4:         4 3
# 5:         5 2

输入 ?dcast 可以查看其他参数和例子的说明。

2.原生的melt/dcast的局限

到目前为止,我们学习了函数 melt 和 dcast 的功能,它们是基于增强包 reshape2 的。但是因为使用了data.table的内部机制(快速排序,二分法搜索等),所以能有效地对data.table实行。
然而,也有一些情况,我们想做的操作无法写得很简洁。比如,考虑下面这个data.table:

DT = fread("melt_enhanced.csv")
DT
#    family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
# 1:         1         30 1998-11-26 2000-01-29         NA             1             2            NA
# 2:         2         27 1996-06-22         NA         NA             2            NA            NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02             2             2             1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21             1             1             1
# 5:         5         29 2000-12-05 2005-02-28         NA             2             1            NA
## 1 = female, 2 = male

如果你想用我们到目前为止学过的知识,将每个孩子的 dob 和 gender 合并到一行中,得这样做:

DT.m1 = melt(DT, id = c("family_id", "age_mother"))
# Warning in melt.data.table(DT, id = c("family_id", "age_mother")): 'measure.vars' [dob_child1,
# dob_child2, dob_child3, gender_child1, gender_child2, gender_child3] are not all of the same
# type. By order of hierarchy, the molten data value column will be of type 'character'. All measure
# variables not of type 'character' will be coerced to. Check DETAILS in ?melt.data.table for more on
# coercion.
DT.m1[, c("variable", "child") := tstrsplit(variable, "_", fixed = TRUE)]
DT.c1 = dcast(DT.m1, family_id + age_mother + child ~ variable, value.var = "value")
DT.c1
#     family_id age_mother  child        dob gender
#  1:         1         30 child1 1998-11-26      1
#  2:         1         30 child2 2000-01-29      2
#  3:         1         30 child3         NA     NA
#  4:         2         27 child1 1996-06-22      2
#  5:         2         27 child2         NA     NA
#  6:         2         27 child3         NA     NA
#  7:         3         26 child1 2002-07-11      2
#  8:         3         26 child2 2004-04-05      2
#  9:         3         26 child3 2007-09-02      1
# 10:         4         32 child1 2004-10-10      1
# 11:         4         32 child2 2009-08-27      1
# 12:         4         32 child3 2012-07-21      1
# 13:         5         29 child1 2000-12-05      2
# 14:         5         29 child2 2005-02-28      1
# 15:         5         29 child3         NA     NA

str(DT.c1) ## gender column is character type now!
# Classes 'data.table' and 'data.frame':    15 obs. of  5 variables:
#  $ family_id : int  1 1 1 2 2 2 3 3 3 4 ...
#  $ age_mother: int  30 30 30 27 27 27 26 26 26 32 ...
#  $ child     : chr  "child1" "child2" "child3" "child1" ...
#  $ dob       : chr  "1998-11-26" "2000-01-29" NA "1996-06-22" ...
#  $ gender    : chr  "1" "2" NA "2" ...
#  - attr(*, ".internal.selfref")=<externalptr> 
#  - attr(*, "sorted")= chr  "family_id" "age_mother" "child"

问题:

  1. 我们想做的是,分别将每个孩子的 dob 和 gender 合并到一行。但是我们先把所有的东西都拆分开了,再将它们合并。很容易看出,这太过迂回和低效了。
    类似的,想想你的壁橱里有4架子的衣服,你想把第1架和第2架的衣服全都放到第1架上,把第3架和第4架的衣服全都放到第3架上。我们刚刚做的事情,就像把4架衣服都放一起,再分开放到第1架和第3架上!
  2. 需要被整合的列可能是不同的类型,在这个例子里面,是字符型和整型。使用函数melt 的时候,这些列被硬塞到结果里面,正如str(DT.c1)的警告消息所提示的,gender列被转化成了字符型。
  3. 我们将variable拆分成了两列,因此额外多了一列,这样做的目的真是非常模糊。我们这么做是因为下一步我们需要转化这一列。
  4. 最后,我们整合了数据。但是问题是我们引入很多操作。特别是,必须要计算等式中变量的顺序,代价太大。

事实上,base::reshape 有简单的写法来实现这个操作。它非常有用,而且经常被低估。你应该试试!

3.增强的新功能

a) 增强的melt

既然我们希望简单地实现同样的操作,我们实现了一个额外的功能,这样就可以同时操作多个列。
-用函数melt 同时拆分多个列
这个办法很简单。我们给参数 measure.vars 传递一个列表,这个列表的每个元素包含需要被合并的列。

colA = paste("dob_child", 1:3, sep = "")
colB = paste("gender_child", 1:3, sep = "")
DT.m2 = melt(DT, measure = list(colA, colB), value.name = c("dob", "gender"))
DT.m2
#     family_id age_mother variable        dob gender
#  1:         1         30        1 1998-11-26      1
#  2:         2         27        1 1996-06-22      2
#  3:         3         26        1 2002-07-11      2
#  4:         4         32        1 2004-10-10      1
#  5:         5         29        1 2000-12-05      2
#  6:         1         30        2 2000-01-29      2
#  7:         2         27        2         NA     NA
#  8:         3         26        2 2004-04-05      2
#  9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3         NA     NA
# 12:         2         27        3         NA     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3         NA     NA

str(DT.m2) ## col type is preserved
# Classes 'data.table' and 'data.frame':    15 obs. of  5 variables:
#  $ family_id : int  1 2 3 4 5 1 2 3 4 5 ...
#  $ age_mother: int  30 27 26 32 29 30 27 26 32 29 ...
#  $ variable  : Factor w/ 3 levels "1","2","3": 1 1 1 1 1 2 2 2 2 2 ...
#  $ dob       : chr  "1998-11-26" "1996-06-22" "2002-07-11" "2004-10-10" ...
#  $ gender    : int  1 2 2 1 2 2 NA 2 1 1 ...
#  - attr(*, ".internal.selfref")=<externalptr>

-函数 patterns()
通常,我们想整合的这些列的列名都有共通的格式。我们可以用函数patterns()指定正则表达式,让语法更简洁。上面的操作还可以这样写:

DT.m2 = melt(DT, measure = patterns("^dob", "^gender"), value.name = c("dob", "gender"))
DT.m2
#     family_id age_mother variable        dob gender
#  1:         1         30        1 1998-11-26      1
#  2:         2         27        1 1996-06-22      2
#  3:         3         26        1 2002-07-11      2
#  4:         4         32        1 2004-10-10      1
#  5:         5         29        1 2000-12-05      2
#  6:         1         30        2 2000-01-29      2
#  7:         2         27        2         NA     NA
#  8:         3         26        2 2004-04-05      2
#  9:         4         32        2 2009-08-27      1
# 10:         5         29        2 2005-02-28      1
# 11:         1         30        3         NA     NA
# 12:         2         27        3         NA     NA
# 13:         3         26        3 2007-09-02      1
# 14:         4         32        3 2012-07-21      1
# 15:         5         29        3         NA     NA

就是这样!

  • 如果需要,我们可以去掉 variable列。
  • 这个功能是用C实现的,因此效率高,节省内存,而且简洁。

b) 增强的dcast

非常好!现在我们可以同时拆分多个列了。现在我们如何将上面的 DT.m2 再恢复成原来的样子呢?
如果我们使用原生的函数dcast(),我们需要做两次,然后将结果合并在一起。但是这样做太麻烦,一点也不简洁和有效。
-同时合并多个 value.vars
我们可以对函数dcast()指定多个 value.var参数,这样操作就在内部进行,而且高效。

## new 'cast' functionality - multiple value.vars
DT.c2 = dcast(DT.m2, family_id + age_mother ~ variable, value.var = c("dob", "gender"))
DT.c2
#    family_id age_mother      dob_1      dob_2      dob_3 gender_1 gender_2 gender_3
# 1:         1         30 1998-11-26 2000-01-29         NA        1        2       NA
# 2:         2         27 1996-06-22         NA         NA        2       NA       NA
# 3:         3         26 2002-07-11 2004-04-05 2007-09-02        2        2        1
# 4:         4         32 2004-10-10 2009-08-27 2012-07-21        1        1        1
# 5:         5         29 2000-12-05 2005-02-28         NA        2        1       NA

说明:

  • 在结果中,原先的属性会尽量保持。
  • 所有的事情都在内部高效处理。快速并且节省内存。

参数fun.aggregate可以指定多个函数:

你可以给函数dcast()的参数fun.aggregate可以指定多个函数。详细内容请执行 ?dcast 来查看示例。

转:任务切换有害论

书摘,来自《软件随想录》,Joel Spolsky

在管理一个程序团队时,第一件要学的事就是任务配置(task allocation)要正确。「任务配置」只是把事情分给大家做的夸大说法。用希伯来文的普通话来说就是「倒文件」(因为你会把文件倒在某人身上)。有些事情做得对会得到不可思议的生产力利益,决定哪些文件要倒在谁身上就是其中之一。反过来没做好的话可能就会陷入麻烦的状况,没有人能做好何任何事情而且大家都抱怨「在这里什么事都做不起来。」

由于这是个针对程序员的网站,我要拿个程序设计问题让你的脑袋动一动暖暖身。

假设你有A和B两件运算要做。每一件都需10秒的CPU时间。现在你有一颗CPU,为了简化问题,所以工作序列中没有其他东西。

在我们的CPU中可以选择是否用多工处理。所以你可以先做好一件再做另一件。

循序处理

运算A
1 2 3 4 5 6 7 8 9 10
运算B
11 12 13 14 15 16 17 18 19 20

也可以使用多工方式。如果用多工的话可以假设这颗特别的CPU每个工作每次可以执行一秒,而且工作切换完全不花时间。

多工处理

运算A
1 3 5 7 9 11 13 15 17 19
运算B
2 4 6 8 10 12 14 16 18 20

你会选哪一种方式呢?大部份人的直觉反应都认为多工比较好。不管哪一种状况,都得等20秒才能两件运算都完成。不过可以想想单就各件运算来说要多久才有结果。

在两种状况下,运算B(标成蓝色)都要20秒才得到结果。不过运算A的结果在多工时需要19秒。可是循序时就只要10秒就好了。

换句话来说在这个安排好的例子中,循序处理的每件运算的平均时间比多工处理少(15秒对19.5秒)。(事实上这例子也并不是真的那么假 - 它是源于Jared在工作上必须解决的一个真实问题。)

方法 运算A花的时间 运算B花的时间 平均
循序处理 10秒 20秒 15
多工处理 19秒 20秒 19.5

我刚刚说过「工作切换完全不花时间」。其实在真的CPU中工作切换是需要一点点时间的,基本上要足够储存CPU暂存器的状态并载入其他工作的CPU暂存器。实际上这短到几乎可以忽略。不过为了让生活更多乐趣,让我们假设工作切换需要半秒。现在情况变得更糟了:

方法 运算A花的时间 运算B花的时间 平均
循序处理 10秒 20秒 + 1次工作切换 = 20.5秒 15.25
多工处理 19秒 + 18次工作切换 = 28秒 20秒 + 19次工作切换 = 29.5秒 28.75

现在呢,虽然我知道这有点蠢,不过就算为了让我高兴一下,想想如果工作切换需要一分钟拿如何?

方法 运算A花的时间 运算B花的时间 平均
循序处理 10秒 20秒 + 1次工作切换 = 80秒 45秒
多工处理 19秒 + 18次工作切换 = 1099秒 20秒 + 19次工作切换 = 1160秒 几近19分钟!!

工作切换用的时间愈长,多工处理的代价愈大。

这件事本身不怎么新奇,不是吗?不久大概就会有些白痴气愤地写信指控我「反对」多工处理了。他们会质问我:「你真的想要回到那种得先结束WordPerfect才能执行Lotus 1-2-3的DOS时代吗?」

不过那并不是我的意思。我只是想要你同意,在这类例子中:

  • 循序处理会让结果平均上比较快得到,而且
  • 工作切换需要愈久,多工处理所付的代价就愈大。

够了,别管CPU了,来管管人吧,这有趣多了。这里的重点在于管理「程序员」时,工作切换会需要很长很长的时间。因为程序设计这种工作必须同时在脑袋里记很多东西。另外记住的东西愈多,写程序时生产力愈高。用全速写程序的程序员脑里随时都会记住无数的事情:变量名称,数据结构,重要的API,写过常要用到的辅助函数名称,甚至存放源代码的次目录名称,一切东西都要记住。如果你把程序员送到克利特岛去度假三星期,他所有东西通通都会忘掉。人脑似乎会把东西移出短期RAM,改存到永远都读不回来的备份磁带上。

要多久呢?嗯,我的软件公司最近放下手头上在做的事(开发一套代号CityDesk的软件产品),花了三星期去帮助某个客户处理一个紧急状况。当我们回到办公室时,感觉好像要另外三星期才能回复全速制作CityDesk。

就个人层次来说,你曾经注意过某件事吗?叫某人做一个工作可以做得很好,可是如果给他两个工作,他会把其中一个做好却忽略另一个,不然就是两件工作都做得很慢,慢到你觉得懒鬼都比他勤劳。这是因为程序设计的工作就是需要很长的切换时间。就我自己来说,当我需要同时完成两个程序设计项目时,切换时间大概要六个小时。以一天八小时来看,等于说多工处理把我的生产力降到每天只剩二小时。真令人沮丧啊。

同样的道理,如果你给某人两件工作,应该要感谢他们只做一件工作而放弃另一件,因为这样能做好更多的事,而且平均上也能更快完成工作。事实上这一切的重点就是绝对不要让人同时做一件以上的事。请确定你有明白它的意思。好的经理人会认为自己的责任是消除障碍,好让大家都能专注在一件事情并把它真的完成。遇到紧急状况时,请先想想能不能自己处理掉,真的不行再丢给深陷在项目中的程序员吧。

data.table 教程4-二级索引和自动索引

目录:

  1. data.table 介绍
  2. 语义引用
  3. 主键、基于二分法搜索的subset
  4. 二次索引和自动索引
  5. 数据拆分和合并

原文地址:
data.table/wiki/Getting-started


感谢G_天星的提醒,貌似现在版本的data.table中还没有setindex()函数,所以可能应该使用set2key()函数。或者通过代码安装data.table试试。


二级索引和自动索引

本教程假定读者已经熟悉data.table的[i, j, by]语法、懂得如何基于二分法的选取了。如果你对这些不熟悉,请学习上面三讲 data.table 介绍语义引用主键、基于快速二分法搜索的subset


数据

我们继续使用已经保存到本地的航班信息flights。

flights <- fread("flights14.csv")
head(flights)
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
# 2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
# 3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
# 4: 2014     1   1        -8       -26      AA    LGA  PBI      157     1035    7
# 5: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
# 6: 2014     1   1         4         0      AA    EWR  LAX      339     2454   18
dim(flights)
# [1] 253316     11

介绍

在这一讲,我们会:

  • 讨论二级索引。
  • 再次演示快速subset,但这次我们使用新的参数on,它能自动设置二级索引。
  • 最后进一步的,来看一下自动索引,它也能自动设置索引,但是它是基于R的原生语法来做subset的。

1.二级索引

a) 什么是二级索引

二级索引和data.table的主键类似,但有以下两点不同:

  • 它不会再内存里将整个data.table重新排序。它只会计算某列的顺序,将这个顺序向量保存在一个额外的,叫做index的属性里面。
  • 一个data.table可以有多个二级索引,这是我们下面要演示的。

b) 设置和获取二级索引

-如何将origin列设置为该data.table的二级索引

setindex(flights, origin)
head(flights)
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
# 2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
# 3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
# 4: 2014     1   1        -8       -26      AA    LGA  PBI      157     1035    7
# 5: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
# 6: 2014     1   1         4         0      AA    EWR  LAX      339     2454   18

## alternatively we can provide character vectors to the function 'setindexv()'
# setindexv(flights, "origin") # useful to program with

# 'index' attribute added
names(attributes(flights))
# [1] "names"             "row.names"         "class"             ".internal.selfref"
# [5] "index"

说明:

  • 函数setindex 和 setindexv()可以对data.table添加一个二级索引。
  • 注意flights实际上没有按照origin列的升序重新排列。还记得吗?setkey()会重新排序!
  • setindex(flights, NULL)会删除所有的二级索引。

-如何取得flights的二级索引

indices(flights)
# [1] "origin"

setindex(flights, origin, dest)
indices(flights)
# [1] "origin"       "origin__dest"

说明:

  • 函数indices()返回一个data.table所有的二级索引。如果该data.table没有二级索引,那么返回NULL。
  • 注意我们对 origin列,dest列创建了另一个二级索引的时候,我们没有丢掉之前创建的第一个二级索引。也就是说,我们可以创建多个二级索引。

c) 为什么使用二级索引

-对一个data.table重新排序成本太高
考虑一下这种情况,当你想用主键origin列来subset所有“JFK”的时候,我们得这么做:

## not run
setkey(flights, origin)
flights["JFK"] # or flights[.("JFK")]

说明:

setkey()需要:
a.计算得出origin列的排序向量,并且
b.基于刚刚的排序向量,对整个data.table重新排序

排序并不是最花时间的,因为data.table使用对整型、字符型、数值型的向量进行radix排序。然而重新排序却很花时间。
除非我们需要对某一列重复地进行subset,否则二分法快速subset的高效可能被重新排序抵消。

-为添加/更新列而对整个data.table重新排序并不理想
-最多只能有一个主键
现在我们如果想对dest列是“LAX”的行,重复地进行某个特定的操作,那么我们必须再调用函数setkey() 设置一次主键。

## not run
setkey(flights, dest)
flights["LAX"]

这样,flights又再次按dest列重新排序了。其实我们真正想做的是,快速地subset同时又不必重新排序。
这时候,二级索引就派上用场了!

-二级索引可以被重用
既然一个data.table中可以有多个二级索引,并且创建一个二级索引就和将一个排序向量保存为属性一样简单,那么创建二级索引后,我们可以省下重新排序的时间。
-参数on使得语法更简洁,并且能自动创建并重用二级索引
我们下面一节会说明参数on的几个优点:

参数on

  • 通过创建索引进行subset。每次都能节省setindex()的时间。
  • 通过检查属性,可以简单地重用已经存在的二级索引。
  • 语法简单。
    注意参数on也可以用来指定主键。事实上,为了更佳的可读性,我们鼓励在参数on里面指定主键。

2.使用参数on和索引进行快速subset

a) 参数i里的subset

-subset所有origin是“JFK”的行

flights["JFK", on = "origin"]
#        year month day dep_delay arr_delay carrier origin dest air_time distance hour
#     1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
#     2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
#     3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
#     4: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
#     5: 2014     1   1        -2       -18      AA    JFK  LAX      338     2475   21
#    ---
# 81479: 2014    10  31        -4       -21      UA    JFK  SFO      337     2586   17
# 81480: 2014    10  31        -2       -37      UA    JFK  SFO      344     2586   18
# 81481: 2014    10  31         0       -33      UA    JFK  LAX      320     2475   17
# 81482: 2014    10  31        -6       -38      UA    JFK  SFO      343     2586    9
# 81483: 2014    10  31        -6       -38      UA    JFK  LAX      323     2475   11

## alternatively
# flights[.("JFK"), on = "origin"] (or) 
# flights[list("JFK"), on = "origin"]

说明:

  • 这段语句执行的subset也是通过创建二级索引,基于快速二分法搜索的。但记住,它不会把这个二级索引自动创建为data.table的一个属性。当然后面我们也会教你如何将它设置为一个属性。
  • 如果我们已经添加了一个二级索引了,那么参数on就可以直接使用这个二级索引,而不是再对整个航班信息flights进行计算。

我们来看下面 verbose = TRUE 的用法:

setindex(flights, origin)
flights["JFK", on = "origin", verbose = TRUE][1:5]
# names(on) = NULL. Assigning 'on' to names(on)' as well.
# Looking for existing (secondary) index... found. Reusing index.
# Starting bmerge ...done in 0 secs
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
# 2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
# 3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
# 4: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
# 5: 2014     1   1        -2       -18      AA    JFK  LAX      338     2475   21

-如何对origin列和dest列进行subset
举个例子,如果我们想选取所有从“JFK”起飞到达“LAX”的所有航班:

flights[.("JFK", "LAX"), on = c("origin", "dest")][1:5]
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1        14        13      AA    JFK  LAX      359     2475    9
# 2: 2014     1   1        -3        13      AA    JFK  LAX      363     2475   11
# 3: 2014     1   1         2         9      AA    JFK  LAX      351     2475   19
# 4: 2014     1   1         2         1      AA    JFK  LAX      350     2475   13
# 5: 2014     1   1        -2       -18      AA    JFK  LAX      338     2475   21

说明:

  • 在参数i里面指定取值,在参数on里面指定列名。参数on必须是一个字符型的向量。
  • 因为计算索引非常快,所以我们不需要使用setindex()。除非你需要对某一列重复地进行subset操作。

b) 参数j里的select

下面我们将要讨论的所有操作,跟我们在上一讲里面学习的类似。只是我们现在使用参数on。
-返回满足条件 origin = “LGA” and dest = “TPA”的 arr_delay列的值

flights[.("LGA", "TPA"), .(arr_delay), on = c("origin", "dest")]
#       arr_delay
#    1:         1
#    2:        14
#    3:       -17
#    4:        -4
#    5:       -12
#   ---          
# 1848:        39
# 1849:       -24
# 1850:       -12
# 1851:        21
# 1852:       -11

c) Chaining

-在上面的基础上,使用chaining来将结果降序排列

flights[.("LGA", "TPA"), .(arr_delay), on = c("origin", "dest")][order(-arr_delay)]
#       arr_delay
#    1:       486
#    2:       380
#    3:       351
#    4:       318
#    5:       300
#   ---          
# 1848:       -40
# 1849:       -43
# 1850:       -46
# 1851:       -48
# 1852:       -49

d) 参数j里的计算

-找出满足条件 origin = “LGA” and dest = “TPA”的 arr_delay列的最大值(航班到达的最长延误时间)

flights[.("LGA", "TPA"), max(arr_delay), on = c("origin", "dest")]
# [1] 486

e) 参数j里使用操作符”:=”进行sub-assign

在上一讲中,我们学习过了类似的功能。同样地,现在我们看看如何找到在flights里面,hours列所有可能的取值:

# get all 'hours' in flights
flights[, sort(unique(hour))]
#  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

可以看到,一共有25种不同的取值。但是0点和24点其实是同样的。下面我们把24全部替换成0,但是这次我们使用参数on。

flights[.(24L), hour := 0L, on = "hour"]

现在我们再来看看24是不是都被替换成0了:

flights[, sort(unique(hour))]
#  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

说明:

  • 这真是二级索引的一大优点。以前,只是为了更新一些行的hour列的取值,我们不得不调用函数setkey()将hour列设置为主键,这必须对整个data.table进行重新排序。但是现在,用参数on,原数据的顺序并没有改变,操作反而更快了!而代码还是如此简洁。

f) 通过参数by聚合

-找到每月从“JFK”起飞的航班起飞的最长延误时间,并按照月份排序

ans <- flights["JFK", max(dep_delay), keyby = month, on = "origin"]
head(ans)
#    month   V1
# 1:     1  881
# 2:     2 1014
# 3:     3  920
# 4:     4 1241
# 5:     5  853
# 6:     6  798

说明:

  • 如果我们不使用二级索引,也就是不在参数on里面指定,那么我们就必须把origin设置为主键。

g) 参数mult

参数mult和上一讲一样。它的默认值是“all”。我们可以选择是第一条还是最后一条符合条件的行被返回。
-subset满足条件dest = “BOS” 和 “DAY”的第一行

flights[c("BOS", "DAY"), on = "dest", mult = "first"]
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014     1   1         3         1      AA    JFK  BOS       39      187   12
# 2: 2014     1   1        25        35      EV    EWR  DAY      102      533   17

-subset满足条件 origin = “LGA” 或者 “JFK” 或者 “EWR”,并且 dest = “XNA” 的最后一行

flights[.(c("LGA", "JFK", "EWR"), "XNA"), on = c("origin", "dest"), mult = "last"]
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014    10  31        -5       -11      MQ    LGA  XNA      165     1147    6
# 2:   NA    NA  NA        NA        NA      NA    JFK  XNA       NA       NA   NA
# 3: 2014    10  31        -2       -25      EV    EWR  XNA      160     1131    6

h) 参数nomatch

如果查询语句没有找到任何匹配的数据,通过指定参数nomatch,我们可以选择是返回 NA,还是忽略。
-在上面这个列子中,忽略没有实际意义的数据

flights[.(c("LGA", "JFK", "EWR"), "XNA"), mult = "last", on = c("origin", "dest"), nomatch = 0L]
#    year month day dep_delay arr_delay carrier origin dest air_time distance hour
# 1: 2014    10  31        -5       -11      MQ    LGA  XNA      165     1147    6
# 2: 2014    10  31        -2       -25      EV    EWR  XNA      160     1131    6

说明:

  • 没有航班从“JFK”起飞到达“XNA”,所以结果里面,这一行被忽略了。

3.自动索引

回顾一下,我们先学习如何通过主键使用快速二分法搜索进行subset。接着,我们学习了使用二级索引,它带来更好的效果,而且语法也更简洁。
等等,有没有更好的方法?有!优化R的原生语法,使用内置的索引。这样我们毋需使用新的语法,就能得到同样的效果。
这就是自动索引。
目前,它只支持操作符 == 和 %in% 。而且只对一列起作用。某一列会被自动创建为索引,并且作为data.table的属性保存起来。这跟参数on不同,参数on会每次创建一个临时索引,所以才会被叫做“二级索引”。

让我们创建一个极大的data.table来凸显它的优势。

set.seed(1L)
dt = data.table(x = sample(1e5L, 1e7L, TRUE), y = runif(100L))
print(object.size(dt), units = "Mb")
# 114.4 Mb

当我们第一次对某一列使用 == 或者 %in% 的时候,会自动创建一个二级索引,它会被用来进行subset。

# have a look at all the attribute names
names(attributes(dt))
# [1] "names"             "row.names"         "class"             ".internal.selfref"

## run thefirst time
(t1 <- system.time(ans <- dt[x == 989L]))
#    user  system elapsed 
#   0.235   0.013   0.249
head(ans)
#      x         y
# 1: 989 0.5372007
# 2: 989 0.5642786
# 3: 989 0.7151100
# 4: 989 0.3920405
# 5: 989 0.9547465
# 6: 989 0.2914710

## secondary index is created
names(attributes(dt))
# [1] "names"             "row.names"         "class"             ".internal.selfref"
# [5] "index"

indices(dt)
# [1] "x"

第一次subset的时候,就是创建索引的时候。因为创建二级索引只会引入一个排序向量,在很多情况下,这种操作符的方式会比扫描向量快得多。所以,从第二次subset开始,自动索引的优势就非常明显了:

## successive subsets
(t2 <- system.time(dt[x == 989L]))
#    user  system elapsed 
#   0.001   0.000   0.001
system.time(dt[x %in% 1989:2012])
#    user  system elapsed 
#   0.001   0.000   0.001

说明:

  • 第一次subset花了0.228秒,但是第二次只花了0.001秒!
  • 可以通过设置全局参数关闭自动索引:options(datatable.auto.index = FALSE)。

我们正在将二分法搜索扩展到其它的操作符,比如 <, <= 和 >=。完成之后,就能直接用在其他操作符上了。
在将来,我们计划将自动索引扩展到参数中的其它列。

在下一讲“结合和滚动结合”中,我们会学习使用主键和二级索引进行快速subset。

为什么程序员要写功能设计书

续上篇,还是阅读了《软件随想录》之后,希望把以前的思考再总结一下。


1.什么是功能设计书

功能设计书
  从用户的角度,完整描述了程序的行为。它只介绍了程序的功能、交互方式,并不涉及具体的实现方式。   

技术设计书
  指引程序员,如何实现功能设计书所描述的功能。它涉及数据结构、数据库、程序语言、算法等等内部的实现细节。   

从头开始设计一个程序时,最重要的,是完成对用户交互的设计:用户需要哪些功能;这些功能具体需要哪些页面来实现;页面如何操作。
功能设计书的写作者需要将整个软件的所有可能的使用场景、流程,在自己的脑海中想象出来,并展现在功能设计书当中。

2.为什么要写功能设计书

有好多同事觉得写设计书没有必要,浪费开发的时间,而且对个人能力没有提升。
我无法想象他们是如何得到这些结论的。可能我在学生时代也是这么想的,看到一个任务就马上写代码,但我早就不这样做了。

2.1 设计

功能设计书最重要作用,就是对程序所有的功能进行设计。通过写文档描述所有页面交互的细节,你会被迫对程序做真正的设计。
在程序的使用中,有可能遇到什么问题,应该如何处理,你在设计阶段就应该想清楚。在写作中,那些可能出问题的地方,大部分都能暴露出来;而如果不写设计书,当你写了一些代码之后,才发现这些问题,付出的代价会更大。
所以设计能够大幅度缩短整个项目的开发周期。

更重要的,这对你个人的思维能力和表达能力都很有提升。
看上去,设计的成果是一篇文档,但为了写作这篇文档,你在脑海中进行的一系列思维活动,才是你最大的收获。

2.2 沟通

有没有回忆起,用户/开发人员/测试人员一次次的跟你确认功能设计的情景?他们东一句西一句地跑过来问你各种愚蠢的问题,你的工作时间被消磨殆尽。
通过功能设计书,你只需要跟别人沟通一次;其他人员只要去阅读功能设计书,就能知道你能给他们的所有信息。
如果没有功能设计书,你只能一遍一遍地讲给他们听。
更可怕的是,如果没有功能设计书,测试人员会根据程序的输入输出来测试程序,或者根据开发人员的实现细节来测试程序,而不是基于设计来设计程序。

2.3 计划

如果没有功能设计书把所有的功能点/页面罗列出来,如何安排计划呢。

2.谁来读功能设计书

客户
  这里的客户,是那些付钱给你开发程序的人。
  友情提醒你一下,要把他们想象成一毛不拔的铁公鸡,妄图只花5块钱买一架航空母舰。
  写作功能设计书的时候,需要一遍又一遍的跟他们确认,跟他们的想象和需求是否一致。这里的功能设计书,就是必须的“立字据”。基本完成之后,再修改是要加钱的哦。
用户
  这里的用户,是那些使用程序的人。
  友情提醒你一下,要把他们想象成又笨又懒的人,他们什么都不会,什么都不想做。
  但你的功能设计书,必须要把他们教会使用这个程序。
技术设计书的作者
  技术设计书的作者,需要根据功能设计书,考虑如何实现程序。
开发人员
  开发人员,主要按照技术设计书来实现程序,但是也会参考功能设计书,确认功能是否都实现了,页面流程是否一致。
测试人员
  测试人员,需要按照功能设计书来写测试案例,并最终实施测试任务。

3.谁来写功能设计书

不同的公司,会有不同的职位和称呼,而且写作功能设计书的人,又不是专职写作功能设计书的。
所以这里,我并不会将什么人来写,而是简单说下,写作功能设计书的人,需要哪些素质。

3.1 技术能力

虽然功能设计书本身并不涉及具体的实现方式,但是,作者必须明白,功能点都是在成本范围内能够实现。这就要求作者必须要有技术功底。
这也是为什么,本文的标题是“为什么程序员要写功能设计书”,有过开发背景的人,才会具备技术功底。其他人员,很可惜,我觉得不具备写作功能设计书的能力。

3.2 思维能力

将脑海中的设计,通过确切的图片和简要的语言,展现在文档中。我认为这不是写作能力,而是思维能力。

3.3 沟通能力

功能设计书不是独自写完就可以的。而是要跟其他所有读者沟通达成一致后,才能最终定稿。

4.如何写功能设计书

4.1 应当包含哪些要素

概述
  该程序的主要功能。
使用场景
  在什么情况下,可以使用该程序。
流程图
  程序各个页面的关系,也是整个服务的全貌。
每个页面的功能说明
  所有页面的详细功能。
细节!细节!细节!
  重要的事情说三遍。页面上所有可能出现的细节都要考虑到,所有可能影响页面的因素都要考虑到。在这些情况下,程序如何反应,写下来。
待解决的问题
  文档不可能在第一版就达到完善的状态,这是你要把还不清楚的地方写下来。
多角度的注解
  主要是从开发人员或者测试人员的角度,需要注意的细节。
修改履历
  所有的读者,都会根据修改履历,大概了解每次修改的功能点和位置。

4.2 写作原则

简单
  能用图片的,尽量用图片而不是语言。能少说的,就不多说。
用户角度
  从用户的角度思考问题,想想用户需要知道什么信息。
评审
  自己阅读,甚至大声朗读几遍。拿给所有必需的读者看。再修改。

软件随想录

这周看了一本很有意思的书,《软件随想录》,其实有两卷,是StackOverflow的创始人和CEO发表的blog文集。
不管是程序员,还是跟程序员打交道的人,都可以读一读,一定会很有收获。

我就不抄书了,谈谈我自己在工作中的一些感想吧。

1.面试题

给小朋友面试的时候,公司会给一些题库,里面有好多经典的面试题,我严重怀疑面试的学生已经背得滚瓜烂熟了,反正基本上都能很快的答上来。但是要是出个笔试题,让他们手写几行(我以为)简单的代码,一天下来十几个人竟然没有一个能完全做对的。
我常常想,这些面试题有什么用?工作中基本不会用到,实在有一些细节的知识点,用的时候再查资料就可以了。
听上去没什么不对的,也许这就是马路上好多IT培训广告的原因,也许这就是公司招近好多不合格的“野生程序员”的原因。
今天看了书才知道,上学时候学习的那些基础课,不是让我们在日后的工作中使用的,而是提前做一次筛选,提醒那些不适合做程序员的人知难而退;然后磨炼留下的人,将他们提高到一个高度,这样以后再做工作上的事,就游刃有余了。

补充一个学习理念:
当你小学三年级的时候,会觉得题目很难;但是当你高中的时候,会觉得小学的题目都很简单。那么,先将自己提升到高中的水平,和那些小学生一起,做小学生的题目。嗯,你一定能够碾压你的竞争对手。

2.成长

刚刚进公司的时候,有“总结-分享-成长”这样的氛围。比如,做项目到一定的阶段,大家会各自总结在这过程中,做得好的不好的地方,分析原因、提出对策,然后在下一次类似的机会中实践;一个人学习了新的知识点,一定会形成文档,下次别人遇到了问题,直接去问他或者看文档,有时还会组织分享会来讲解;如此这般,很容易感受到自身的成长。
而现在,大家都分散着做各自的项目,几乎没有时间和渠道交流。离职率也很高,各自学习自己感兴趣的东西。这也导致完全无心来培养一个新手,反正培养出来了也是要走的。哼哼。

3.是geek还是农民工

和程序员以外的人交流,噢,简直没法交流啊。就连本来应该懂一点技术的产品经理,也是拿各种不相关的词汇生搬硬套,并且妄图做技术上的决策。业务部的人,简直把程序员当成实现他们需求的工具,而且常常会想用1块钱的成本做10块钱的事,还觉得只值5毛钱。哼哼。
所以理想的情况,还是待在geek文化比较强烈的公司吧。


亚马逊链接:
软件随想录 I
软件随想录 II