一文学会机器学习预测流程(电信客户流失率问题)

摘要: 本文介绍了如何把机器学习算法应用到具体问题中。 以电信运营商客户流失率问题为例,从问题的提出, 数据的分析, 算法的评估, 到最终的结果展示, ,一步步介绍机器学习基本流程。 用户数据来源于互联网。

1 定义问题

客户流失率问题是电信运营商面临得一项重要课题,也是一个较为流行的案例。根据测算,招揽新的客户比保留住既有客户的花费大得多(通常5-20倍的差距)。因此,如何保留住现在的客户对运营商而言是一项非常有意义的事情。 本文希望通过一个公开数据的客户流失率问题分析,能够带着大家理解如何应用机器学习预测算法到实际应用中。

当然, 实际的场景比本文例子复杂的多,如果想具体应用到项目, 还需要针对不同的场景和数据进行具体的分析。

从机器学习的分类来讲, 这是一个监督问题中的分类问题。 具体来说, 是一个二分类问题。 所有的数据中包括一些特征, 最后就是它的分类:流失或者在网。接下来我们就开始具体的处理。

2 分析数据

首先我们来导入数据, 然后查看数据的基本情况。

2.1 数据导入

通过pandas来导入csv, 然后我们来查看一下数据的基本情况

from __future__ import division
import pandas as pd
import numpy as np

ds = pd.read_csv(‘./churn.csv‘)
col_names = ds.columns.tolist()
print "Column names:"
print col_names
print(ds.shape)

输出:

Column names:
[‘State‘, ‘Account Length‘, ‘Area Code‘, ‘Phone‘, "Int‘l Plan", ‘VMail Plan‘, ‘VMail Message‘, ‘Day Mins‘, ‘Day Calls‘, ‘Day Charge‘, ‘Eve Mins‘, ‘Eve Calls‘, ‘Eve Charge‘, ‘Night Mins‘, ‘Night Calls‘, ‘Night Charge‘, ‘Intl Mins‘, ‘Intl Calls‘, ‘Intl Charge‘, ‘CustServ Calls‘, ‘Churn?‘]
(3333, 21)

可以看到, 整个数据集有3333条数据, 20个维度, 最后一项是分类。

2.2 基本信息以及类型

我们可以打印一些数据, 对数据和取值有一个基本的理解。

peek = data.head(5)
print(peek)

输出:

   State  Account Length  Area Code     Phone Int‘l Plan VMail Plan  0     KS             128        415  382-4657         no        yes
1     OH             107        415  371-7191         no        yes
2     NJ             137        415  358-1921         no         no
3     OH              84        408  375-9999        yes         no
4     OK              75        415  330-6626        yes         no
    Eve Charge  Night Mins  Night Calls  Night Charge  Intl Mins  Intl Calls  0        16.78       244.7           91         11.01       10.0           3
1        16.62       254.4          103         11.45       13.7           3
2        10.30       162.6          104          7.32       12.2           5
3         5.26       196.9           89          8.86        6.6           7
4        12.61       186.9          121          8.41       10.1           3
    Intl Charge  CustServ Calls  Churn?
0          2.70               1  False.
1          3.70               1  False.
2          3.29               0  False.
3          1.78               2  False.
4          2.73               3  False.  

我们可以看到, 数据集有20项特征,分别是州名, 账户长度, 区号, 电话号码, 国际计划,语音邮箱, 白天通话分钟数, 白天电话个数, 白天收费, 晚间通话分钟数,晚间电话个数, 晚间收费, 夜间通话分钟数,夜间电话个数, 夜间收费, 国际分钟数, 国际电话个数, 国际收费, 客服电话数,流失与否.

  • 可以看到这里面有个人信息,应该可以看到有些信息与流失与否关系不大。 州名, 区号可以指明客户的位置, 和流失有关系么, 不知道, 具体位置如果不分类, 应该完全没有关系。 而州名, 也许某个州有了某个强劲的竞争对手? 这也是瞎猜, 暂时意义不大, 删除。
  • 账号长度, 电话号码, 不需要
  • 国际计划, 语音邮箱。 可能有关系, 先留着吧。
  • 分别统计了白天, 晚间, 夜间的通话分钟, 电话个数, 收费情况。 这是重要信息保留
  • 客服电话, 客户打电话投诉多那流失率可能会大。 这个是重要信息保留。
  • 流失与否。 这是分类结果。

然后我们可以看一下数据的类型, 如下:

ds.info()

输出:

RangeIndex: 3333 entries, 0 to 3332
Data columns (total 21 columns):
State             3333 non-null object
Account Length    3333 non-null int64
Area Code         3333 non-null int64
Phone             3333 non-null object
Int‘l Plan        3333 non-null object
VMail Plan        3333 non-null object
VMail Message     3333 non-null int64
Day Mins          3333 non-null float64
Day Calls         3333 non-null int64
Day Charge        3333 non-null float64
Eve Mins          3333 non-null float64
Eve Calls         3333 non-null int64
Eve Charge        3333 non-null float64
Night Mins        3333 non-null float64
Night Calls       3333 non-null int64
Night Charge      3333 non-null float64
Intl Mins         3333 non-null float64
Intl Calls        3333 non-null int64
Intl Charge       3333 non-null float64
CustServ Calls    3333 non-null int64
Churn?            3333 non-null object
dtypes: float64(8), int64(8), object(5)
memory usage: 546.9+ KB

看见, 有int, float, object。 对于不是数据型的数据, 后面除非决策树等算法, 否则应该会转化成数据行。

所以我们把churn? 结果转化, 以及"Int‘l Plan","VMail Plan", 这两个参数只有yes, no 两种, 所以也进行转化成01值。

2.3 描述性统计

describe() 可以返回具体的结果, 对于每一列。

数量

平均值

标准差

25% 分位

50% 分位数

75% 分位数

最大值 很多时候你可以得到NA的数量和比例。

       Account Length    Area Code  VMail Message     Day Mins    Day Calls  count     3333.000000  3333.000000    3333.000000  3333.000000  3333.000000
mean       101.064806   437.182418       8.099010   179.775098   100.435644
std         39.822106    42.371290      13.688365    54.467389    20.069084
min          1.000000   408.000000       0.000000     0.000000     0.000000
25%         74.000000   408.000000       0.000000   143.700000    87.000000
50%        101.000000   415.000000       0.000000   179.400000   101.000000
75%        127.000000   510.000000      20.000000   216.400000   114.000000
max        243.000000   510.000000      51.000000   350.800000   165.000000   

        Day Charge     Eve Mins    Eve Calls   Eve Charge   Night Mins  count  3333.000000  3333.000000  3333.000000  3333.000000  3333.000000
mean     30.562307   200.980348   100.114311    17.083540   200.872037
std       9.259435    50.713844    19.922625     4.310668    50.573847
min       0.000000     0.000000     0.000000     0.000000    23.200000
25%      24.430000   166.600000    87.000000    14.160000   167.000000
50%      30.500000   201.400000   100.000000    17.120000   201.200000
75%      36.790000   235.300000   114.000000    20.000000   235.300000
max      59.640000   363.700000   170.000000    30.910000   395.000000   

       Night Calls  Night Charge    Intl Mins   Intl Calls  Intl Charge  count  3333.000000   3333.000000  3333.000000  3333.000000  3333.000000
mean    100.107711      9.039325    10.237294     4.479448     2.764581
std      19.568609      2.275873     2.791840     2.461214     0.753773
min      33.000000      1.040000     0.000000     0.000000     0.000000
25%      87.000000      7.520000     8.500000     3.000000     2.300000
50%     100.000000      9.050000    10.300000     4.000000     2.780000
75%     113.000000     10.590000    12.100000     6.000000     3.270000
max     175.000000     17.770000    20.000000    20.000000     5.400000   

       CustServ Calls
count     3333.000000
mean         1.562856
std          1.315491
min          0.000000
25%          1.000000
50%          1.000000
75%          2.000000
max          9.000000  

2.4 图形化理解你的数据

之前的一些信息, 只是一些很初步的理解, 但是对于机器学习算法来讲是不够的。 下面我们从几个维度去进一步理解你的数据。工具可以用数字表格, 也可以用图形(matplotlib) 这里画图较多。

  • 特征自己的信息
  • 特征和分类之间的关系
  • 特征和特征之间的关系

    -- 这里鉴于时间的关系, 有些关系并没有直接应用于算法本身, 但是在进一步的算法提升中是很有意义的, 这里更多的是一种展示。

2.4.1 特征本身的信息

我们先来看一下流失比例, 以及关于打客户电话的个数分布

import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure()
fig.set(alpha=0.2)  # 设定图表颜色alpha参数

plt.subplot2grid((2,3),(0,0))             # 在一张大图里分列几个小图
ds[‘Churn?‘].value_counts().plot(kind=‘bar‘)# plots a bar graph of those who surived vs those who did not.
plt.title(u"stat for churn") # puts a title on our graph
plt.ylabel(u"number")  

plt.subplot2grid((2,3),(0,2))
ds[‘CustServ Calls‘].value_counts().plot(kind=‘bar‘)# plots a bar graph of those who surived vs those who did not.
plt.title(u"stat for cusServCalls") # puts a title on our graph
plt.ylabel(u"number")  

plt.show()

很容易理解。

然后呢, 我们的数据的特点是对白天, 晚上, 夜间,国际都有分钟数, 电话数, 收费三种维度。 那么我们拿白天的来举例。

import matplotlib.pyplot as plt
%matplotlib inline
fig = plt.figure()
fig.set(alpha=0.2)  # 设定图表颜色alpha参数

plt.subplot2grid((2,5),(0,0))             # 在一张大图里分列几个小图
ds[‘Day Mins‘].plot(kind=‘kde‘)    # plots a kernel desnsity estimate of customer
plt.xlabel(u"Mins")# plots an axis lable
plt.ylabel(u"density")
plt.title(u"dis for day mins")

plt.subplot2grid((2,5),(0,2))
ds[‘Day Calls‘].plot(kind=‘kde‘)    # plots a kernel desnsity estimate of customer
plt.xlabel(u"call")# plots an axis lable
plt.ylabel(u"density")
plt.title(u"dis for day calls")

plt.subplot2grid((2,5),(0,4))
ds[‘Day Charge‘].plot(kind=‘kde‘)   # plots a kernel desnsity estimate of customer
plt.xlabel(u"Charge")# plots an axis lable
plt.ylabel(u"density")
plt.title(u"dis for day charge")

plt.show()

可以看到分布基本上都是高斯分布, 这也符合我们的预期, 而高斯分布对于我们后续的一些算法处理是个好消息。

2.4.2 特征和分类的关联

我们来看一下一些特征和分类之间的关联。 比如下面int plan

import matplotlib.pyplot as plt
fig = plt.figure()
fig.set(alpha=0.2)  # 设定图表颜色alpha参数

int_yes = ds[‘Churn?‘][ds[‘Int\‘l Plan‘] == ‘yes‘].value_counts()
int_no = ds[‘Churn?‘][ds[‘Int\‘l Plan‘] == ‘no‘].value_counts()
df_int=pd.DataFrame({u‘int plan‘:int_yes, u‘no int plan‘:int_no})
df_int.plot(kind=‘bar‘, stacked=True)
plt.title(u"statistic between int plan and churn")
plt.xlabel(u"int or not")
plt.ylabel(u"number")

plt.show()

我们可以看到, 有国际电话的流失率较高。 猜测也许他们有更多的选择, 或者对服务有更多的要求。 需要特别对待。 也许你需要电话多收集一下意见了。

再来看一下

#查看客户服务电话和结果的关联
fig = plt.figure()
fig.set(alpha=0.2)  # 设定图表颜色alpha参数

cus_0 = ds[‘CustServ Calls‘][ds[‘Churn?‘] == ‘False.‘].value_counts()
cus_1 = ds[‘CustServ Calls‘][ds[‘Churn?‘] == ‘True.‘].value_counts()
df=pd.DataFrame({u‘churn‘:cus_1, u‘retain‘:cus_0})
df.plot(kind=‘bar‘, stacked=True)
plt.title(u"Static between customer service call and churn")
plt.xlabel(u"Call service")
plt.ylabel(u"Num") 

plt.show()

基本上可以看出, 打客户电话的多少和最终的分类是强相关的, 打电话3次以上的流失率比例急速升高。 这是一个非常关键的指标。

3 准备数据

好的, 我们已经看了很多,对数据有了一定的理解。 下面我们开始具体对数据进行操作。

3.1 去除无关列

首先, 根据对问题的分析, 我们做第一件事情, 去除三列无关列。 州名, 电话, 区号。

  • 我们和下一步一起做

3.2 转化成数值类型

对于有些特征, 本身不是数值类型的, 这些数据是不能被算法直接使用的, 所以我们来处理一下

# Isolate target data
ds_result = ds[‘Churn?‘]
Y = np.where(ds_result == ‘True.‘,1,0)

dummies_int = pd.get_dummies(ds[‘Int\‘l Plan‘], prefix=‘_int\‘l Plan‘)
dummies_voice = pd.get_dummies(ds[‘VMail Plan‘], prefix=‘VMail‘)

ds_tmp=pd.concat([ds, dummies_int, dummies_voice], axis=1)

# We don‘t need these columns
to_drop = [‘State‘,‘Area Code‘,‘Phone‘,‘Churn?‘, ‘Int\‘l Plan‘, ‘VMail Plan‘]
df = ds_tmp.drop(to_drop,axis=1)

print "after convert "
print df.head(5)

输出:

after convert 01
   Account Length  VMail Message  Day Mins  Day Calls  Day Charge  Eve Mins  0             128             25     265.1        110       45.07     197.4
1             107             26     161.6        123       27.47     195.5
2             137              0     243.4        114       41.38     121.2
3              84              0     299.4         71       50.90      61.9
4              75              0     166.7        113       28.34     148.3   

   Eve Calls  Eve Charge  Night Mins  Night Calls  Night Charge  Intl Mins  0         99       16.78       244.7           91         11.01       10.0
1        103       16.62       254.4          103         11.45       13.7
2        110       10.30       162.6          104          7.32       12.2
3         88        5.26       196.9           89          8.86        6.6
4        122       12.61       186.9          121          8.41       10.1   

   Intl Calls  Intl Charge  CustServ Calls  _int‘l Plan_no  _int‘l Plan_yes  0           3         2.70               1               1                0
1           3         3.70               1               1                0
2           5         3.29               0               1                0
3           7         1.78               2               0                1
4           3         2.73               3               0                1   

   VMail_no  VMail_yes
0         0          1
1         0          1
2         1          0
3         1          0
4         1          0 

我们可以看到结果, 所有的数据都是数值型的, 而且除去了对我们没有意义的列。

3.3 scale 数据范围

我们需要做一些scale的工作。 就是有些属性的scale 太大了。

  • 对于逻辑回归和梯度下降来说, 个属性的scale 差距太大, 会对收敛速度有很大的影响。
  • 我们这里对所有的都做, 其实可以对一些突出的特征做这种处理。
#scale
X = df.as_matrix().astype(np.float)

# This is important
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X = scaler.fit_transform(X)

print "Feature space holds %d observations and %d features" % X.shape
print "Unique target labels:", np.unique(y)

输出:

Feature space holds 3333 observations and 19 features
Unique target labels: [0 1]

其他的呢, 还可以考虑降维等各种方式。 但是再实际使用中, 我们往往首先做出一个模型, 得到一个参考结果, 然后逐步优化。 所以我们准备数据就到这里。

4 评估算法

我们会使用多个算法来计算结果, 然后选择较好的。 如下

# prepare models
models = []
models.append((‘LR‘, LogisticRegression()))
models.append((‘LDA‘, LinearDiscriminantAnalysis()))
models.append((‘KNN‘, KNeighborsClassifier()))
models.append((‘CART‘, DecisionTreeClassifier()))
models.append((‘NB‘, GaussianNB()))
models.append((‘SVM‘, SVC()))
# evaluate each model in turn
results = []
names = []
scoring = ‘accuracy‘
for name, model in models:
    kfold = KFold(n_splits=10, random_state=7)
    cv_results = cross_val_score(model, X, Y, cv=kfold, scoring=scoring)
    results.append(cv_results)
    names.append(name)
    msg = "%s: %f (%f)" % (name, cv_results.mean(), cv_results.std())
    print(msg)
# boxplot algorithm comparison
fig = pyplot.figure()
fig.suptitle(‘Algorithm Comparison‘)
ax = fig.add_subplot(111)
pyplot.boxplot(results)
ax.set_xticklabels(names)
pyplot.show()
LR: 0.860769 (0.021660)
LDA: 0.852972 (0.021163)
KNN: 0.896184 (0.016646)
CART: 0.920491 (0.012471)
NB: 0.857179 (0.015487)
SVM: 0.921091 (0.016828)

可以看到什么呢, 看到SVM 和 CART 效果相对较好。

5 提升结果

提升的部分, 如何使用提升算法。

比如随机森林。 xgboost

from sklearn.ensemble import RandomForestClassifier
num_trees = 100
max_features = 3
kfold = KFold(n_splits=10, random_state=7)
model = RandomForestClassifier(n_estimators=num_trees, max_features=max_features)
results = cross_val_score(model, X, Y, cv=kfold)
print(results.mean())
# 0.954696013379
from sklearn.ensemble import GradientBoostingClassifier
seed = 7
num_trees = 100
kfold = KFold(n_splits=10, random_state=seed)
model = GradientBoostingClassifier(n_estimators=num_trees, random_state=seed)
results = cross_val_score(model, X, Y, cv=kfold)
print(results.mean())
# 0.953197209185

可以看到, 这两种算法对单个算法的提升还是很明显的。 进一步的, 也可以继续调整tree的数目, 但是效果应该差不多了。

6 展示结果

这里展示了如何保存这个算法, 以及如何取出然后应用。

#store
from sklearn.model_selection import train_test_split
from pickle import dump
from pickle import load

X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.33, random_state=7)
from sklearn.ensemble import GradientBoostingClassifier
seed = 7
num_trees = 100
kfold = KFold(n_splits=10, random_state=seed)
model = GradientBoostingClassifier(n_estimators=num_trees, random_state=seed)
model.fit(X_train, Y_train)
# save the model to disk
filename = ‘finalized_model.sav‘
dump(model, open(filename, ‘wb‘))
# some time later...
# load the model from disk
loaded_model = load(open(filename, ‘rb‘))
result = loaded_model.score(X_test, Y_test)
print(result)

7 后记

本文展示了通过用户流失率问题, 如何把机器学习的预测过程应用到实际项目中。

  • 从业务的角度, 这个只是一个demo性质的应用, 实际场景可能复杂的多。
  • 从流程的角度, 通过对数据的分析可以进一步提升算法的性能, 对于某些的特征, 可以采取不同的处理方式。 比如缺失值的处理, 这里很完整, 就省去了这个步骤。
  • 对于一些步骤, 这里解释并不是很详细, 读者可以参考博客的 掌握python机器学习 系列

参考文章

1 http://blog.yhat.com/posts/predicting-customer-churn-with-sklearn.html

2 http://blog.csdn.net/han_xiaoyang/article/details/49797143 寒小阳的博客。

原文地址:https://www.cnblogs.com/zzbbnj/p/8111438.html

时间: 2024-11-07 11:22:44

一文学会机器学习预测流程(电信客户流失率问题)的相关文章

简信CRM:CRM系统为企业减少客户流失率

客户是企业盈利的来源,对于客户资源的竞争在逐渐增加,但是很多企业都面临着客户流失的问题.对于企业客户而言,有一定的的流失率是必然的.那么,如何有效的减少客户流失率的问题,就成了很多企业都想要解决的难题.针对这一问题,简信CRM客户管理系统就能很有效降低客户流失率?下面我们一起来了解下吧! 1.客户信息储存销售人员自己获得的客户资源.公司营销活动获得的客户资源以及官网访问获得的客户资源,都被录入在简信CRM客户管理系统之中,存放在终端数据库.不仅可以让管理员方便快速的查看客户的信息,而且还有效的防

拓端数据tecdat:DT模型打好用户流失预防针——电信客户流失浅析

原文链接 消费者今天会订阅多个电信服务(电视.付费节目.游戏等).然而电信供应商提供的服务的差异化程度不高,客户忠诚度成为问题. 因此,电信公司主动识别有流失倾向的客户,并采取预防措施来保留这些客户变得越来越重要. 基于以上背景,tecdat研究人员对16年至今的电信用户流失数据进行分析,并建立预测模型,识别出流失概率很高的客户群体,同时找出哪些用户特征("X")对用户流失("Y")会有重大影响. ▼ ▍数据概览 使用收视服务的用户最多,订阅付费节目其次,订阅游戏业

基于Apache Spark机器学习的客户流失预测

流失预测是个重要的业务,通过预测哪些客户可能取消对服务的订阅来最大限度地减少客户流失.虽然最初在电信行业使用,但它已经成为银行,互联网服务提供商,保险公司和其他垂直行业的通用业务. 预测过程是大规模数据的驱动,并且经常结合使用先进的机器学习技术.在本篇文章中,我们将看到通常使用的哪些类型客户数据,对数据进行一些初步分析,并生成流失预测模型 - 所有这些都是通过Spark及其机器学习框架来完成的. 使用数据科学更好地理解和预测客户行为是一个迭代过程,其中涉及: 1.发现和模型创建: 分析历史数据.

机器学习项目流程(一)

机器学习项目流程 在这我们会从头开始做一个机器学习项目,向大家展示一个机器学习项目的一个基本流程与方法.一个机器学习主要分为以下几个步骤: 从整体上了解项目 获取数据 发现并可视化数据,以深入了解数据 为机器学习算法准备数据 选择模型并训练 模型调优 展示解决方案 部署.监控.以及维护我们的系统 我们不会遍历所有步骤,仅从一个例子展示一个常规的流程. 使用真实数据 在学习机器学习时,最好是使用真实数据,不要用人工代码生成的模拟数据.一些可以获取数据的地方如: 开放数据集 UC Irvine Ma

携程客户流失概率预测 赛后总结

这次和一群好友在科赛网上参加了携程的客户流失概率预测比赛,最终33名完赛,虽然依然是渣渣成绩,但又认识了好多大神,学到了不少东西. 这次这个比赛其实难点还不少,首先就是评分规则与众不同,本来以为会用AUC或者F1 score之类的,但官方用的是precision≥97%下,recall的最大值.起初也不知道如何自定义metric,只能用一些默认的,跟线上完全对应不起来,很摸瞎. 一开始只觉得是简单的分类问题,也没太多想,处理一下特征,先跑个分类器吧.自从上次在天池吃瘪了之后,这次在特征处理上算是

KNIMI数据挖掘建模与分析系列_004_利用KNIMI做客户流失预测

利用KNIMI做客户流失预测 老帅 20150801 http://blog.csdn.net/shuaihj 一.测试数据 中国移动客服数据 需要测试数据,请留下邮箱 二.统计已流失客户 1.读取移动客服数据(客户流失.xlsx) 2.统计已流失客户 参数设置 统计结果 3.数据流 三.贝叶斯预测客户流失 1.字符类型转换 将"流失"列转换为字符串类型 2.划分训练集和测试集 取30%作为训练数据,剩余70%作为测试数据,我们将预测这70%客户的流失率: 设置"流失&quo

使用机器学习预测天气(第三部分神经网络)

概述??这是使用机器学习预测平均气温系列文章的最后一篇文章了,作为最后一篇文章,我将使用google的开源机器学习框架tensorflow来构建一个神经网络回归器.关于tensorflow的介绍.安装.入门,请自己google,这里就不做讲述.??这篇文章我主要讲解一下几点:了解人工神经网络理论tensorflow高级API:Estimators构建DNN模型预测天气人工神经网络基础理论??上一篇文章主要讲解了如何构建线性回归模型(这是最基础的机器学习算法)来预测内布拉斯加州林肯市每天的平均气温

分享《Python机器学习—预测分析核心算法》高清中文版PDF+高清英文版PDF+源代码

下载:https://pan.baidu.com/s/1sfaOZmuRj14FWNumGQ5ahw 更多资料分享:http://blog.51cto.com/3215120 <Python机器学习-预测分析核心算法>高清中文版PDF+高清英文版PDF+源代码高清中文版,338页,带目录和书签,文字能够复制粘贴.高清英文版,361页,带目录和书签,文字能够复制粘贴.中英文两版对比学习.配套源代码.经典书籍,讲解详细. 其中,高清中文版如图所示: 原文地址:http://blog.51cto.c

《Python机器学习—预测分析核心算法》高清中文版PDF+高清英文版PDF+源代码

资源链接:https://pan.baidu.com/s/1J61jH-fqwGceoId6F8mr5Q<Python机器学习-预测分析核心算法>高清中文版PDF+高清英文版PDF+源代码高清中文版,338页,带目录和书签,文字能够复制粘贴.高清英文版,361页,带目录和书签,文字能够复制粘贴.中英文两版对比学习.配套源代码.经典书籍,讲解详细.其中,高清中文版如图所示: 原文地址:http://blog.51cto.com/14063572/2317037