Customer Profile Insights & repurchase related insights

  • Project Overview
  • Main Processes of the Project
  • Links to the Project
  • Other Materials

Data Preprocessing(数据预处理)

In [1]:
# import the libraries we might need
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.impute import SimpleImputer
from matplotlib.pyplot import MultipleLocator

import xgboost as xgb
from xgboost import XGBClassifier
from sklearn.model_selection import train_test_split
from sklearn.model_selection import KFold
from sklearn.preprocessing import LabelEncoder
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import PolynomialFeatures
from sklearn.preprocessing import StandardScaler
from sklearn.feature_selection import VarianceThreshold
from sklearn.feature_selection import SelectFromModel
from sklearn.utils import shuffle
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.metrics import roc_curve, auc
from sklearn import metrics   #Additional scklearn functions
from sklearn.model_selection import GridSearchCV, cross_validate  #Perforing grid search
from matplotlib.legend_handler import HandlerLine2D

# basic set up
sns.set_style('darkgrid')
%matplotlib inline
from matplotlib.pylab import rcParams
rcParams['figure.figsize'] = 12, 8
plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus'] = False
pd.set_option('display.max_columns', 100)
In [2]:
# find the path
import os
print(os.path.abspath('.'))
C:\Users\gzjgz\Documents
In [3]:
# load the data
path = r'C:\Users\gzjgz\OneDrive\Documents\rawdata.xls'
df_key_customer_info = pd.read_excel(path,sheet_name = '车主')
df_repurchased = pd.read_excel(path,sheet_name = '车辆')
df_service_request = pd.read_excel(path,sheet_name = '工单')

Basic Inspection of the Data

In [4]:
# check the data
df_key_customer_info.head()
Out[4]:
车主id 姓名 姓名拼音 英文名称 性别 生日 婚姻状态 教育程度 职业大类 职业小类 职位 行业 兴趣爱好 家庭年收入 家庭成员人数 计划考驾照的时间 是否拥有驾照 取得驾照年月 是否大客户 是否VIP客户 手机 区号 分机 省份 城市 址区 邮编
0 1-8MN-68814 ZHU BO HE . NaT NaN 初中 NaN NaN 总裁/总经理/总监/企业高管 家居、装饰 NaN NaN Four NaN Y NaT NaN NaN 2.717980e+10 NaN NaN 北京市 北京市 房山区 102401
1 1-3YPW-6314 HUANG AN - 1980-02-14 NaN 本科 NaN NaN 公司拥有者(老板)/合伙人 NaN NaN NaN NaN NaN NaN NaT NaN NaN 2.781596e+10 NaN NaN 江西省 景德镇市 珠山区 NaN
2 1-8MN-72734 SU XIAO LIAN - 1981-12-12 NaN 大专 NaN NaN NaN 家居、装饰 NaN NaN NaN NaN Y NaT NaN NaN 2.740399e+10 NaN NaN NaN NaN NaN 201204
3 1-1SB-15357 NaN . NaT NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaT NaN NaN 2.767710e+10 NaN NaN 湖北省 十堰市 NaN 442700
4 1-8LJ-21917 WANG PEI . NaT NaN 高中 NaN NaN 总裁/总经理/总监/企业高管 电气、电器、仪器制造行业 NaN 8-12万元(含12万) No Answer NaN NaN NaT NaN NaN 3.102531e+10 NaN NaN 河北省 衡水市 武强县 53000
In [5]:
df_repurchased.head()
Out[5]:
车主ID VVIN 市场车型 车型 购买日期 开票价格 ASSET_REF_EXPR OU_NAME OU_ABREV OU_CITY OU_COUNTY
0 1-1DL4-1160 F2067851 全新途观 途观丝绸之路版 300TSI 自动 前驱 风尚版 2015-04-20 263000.0 大众 南充明云众聚车业有限公司 四川省 南充市 高坪区
1 1-3FZ0-4739 FN014177 2017款昕锐 昕锐 1.6L 手动 乐选版 2015-02-02 122000.0 斯柯达 河北信实汽车销售服务有限公司 河北省 石家庄市 裕华区
2 1-31O3-957 EN172003 全新帕萨特 A423L7+W30-全新帕萨特 30周年纪念版 2014-12-09 260600.0 大众 武汉恒信楚雄汽车销售服务有限公司 湖北省 武汉市 洪山区
3 1-2TG5-2075 FN035631 凌渡 凌渡 330TSI DSG 舒适版 2015-05-29 237900.0 大众 海南信兴汽车销售有限公司 海南省 海口市 龙华区
4 1-1FR6-9027 EN019851 全新帕萨特 全新帕萨特 2.0T DSG 至尊版 2014-02-24 322800.0 大众 徐州恒运汽车销售服务有限公司 江苏省 徐州市 云龙区
In [6]:
df_service_request.head()
Out[6]:
车主ID 服务品牌 服务工单编号 关联工单号 工单状态 性质分类 业务分类 工单产生类型 工单一级分类 工单二级分类 工单三级分类 工单来源 工单创建日期 工单来电描述 工单处理层面 处理部门 省份 车型大类 车型 车型6位码 车色 购车时间 车辆VIN码 行驶里程 涉及机构类型 涉及机构代码 涉及机构名称 所属分销中心 当前处理人 当前处理机构 当前处理经销商 上次处理人工号 当前处理人工号 故障产品分类 工单实际响应时间 工单实际完成时间 是否及时响应 是否及时完成 是否一次解决 是否记录投诉次数 是否重大事故 操作历史当前操作部门 操作历史当前操作人 操作历史当前操作时间 操作历史操作类型 系统登陆用户名 姓名 是否媒体工单 媒体类别一级 媒体类别二级 媒体名称 是否与召回有关 是否普遍问题 最后一次操作内容 用户咨询内容
0 1-8GY-98256 大众 ECS20180318111336-9542224053 NaN 处理完成 咨询 流转 主动来电 促销及市场活动 NaN NaN 语音类 2018-03-18 11:13:49 2018 3 用户来电咨询2011-2015款部分全新帕萨特燃油泵控制单元召回表示没有工作人员联系,之后安... NaN NaN 浙江省 帕萨特 VF351-全新帕萨特 1.8T DSG 御尊版+选装包2(黑色) VF351 黑色系 2011-09-07 BN235406 85561.0 NaN NaN NaN NaN 7698 NaN NaN NaN 7698 NaN NaT 2018-03-18 11:19:51 N N N N N 客服中心-CC 7698 2018-03-18 11:19:51 工单关闭 7698 7698 N NaN NaN NaN Y N NaN 燃油泵控制单元->召回通知方式->咨询召回通知方式
1 1-8ML-63634 大众 ECS20180531142228-10006668063-2 ECS20180403112416-9642553764 处理完成 投诉 售后 主动来电 产品技术/质量 质量担保异议(担保期外索赔纠纷) NaN 语音类 2018-05-31 14:22:28 2018 5 用户再次来电表示车辆在召回之前进行过燃油泵控制单元的自费更换,先收到燃油泵召回,去了SST只... 总部 大众售后-SVAT 山东省 NaN 大众0101 VW0101 NaN 2011-08-11 BN218048 100000.0 维修站 74314120.0 东营石大汽车销售服务有限公司 山东RSSC售后 孙宏鑫 大众售后-SVAT 东营石大汽车销售服务有限公司 7648 SUNHONGXIN 发动机 2018-06-04 17:05:22 2018-06-04 17:05:22 N Y N N N VW售后SE 孙宏鑫 2018-06-04 17:05:21 工单处理 7648 7648 N NaN NaN NaN Y N 已指导处理 退还客户费用 燃油泵控制单元->召回措施->召回维修方案
2 1-8I9-19381 斯柯达 ECS20190701140245-11695697624 NaN 处理完成 咨询 分配 主动来电 维修保养相关 NaN NaN 语音类 2019-07-01 14:02:50 2019 7 何先生来电咨询准备购买车辆询问质保是否是3年10万公里,及什么是延长质量担保计划? 经销商告... NaN NaN 上海市 明锐 新明锐1.8TSI双离合器手自动一体逸尊版 9133XD 灰色系 2010-12-08 A2793190 66000.0 NaN NaN NaN NaN 熊桃桃 NaN NaN NaN 7140 NaN NaT 2019-07-01 14:03:05 N N N N N 客服中心-CC 7140 2019-07-01 13:57:40 NaN 7140 熊桃桃 N NaN NaN NaN N N NaN NaN
3 1-3MLV-1771 大众 ECS20190208205023-11244767295 NaN 处理完成 咨询 分配 主动来电 产品技术质量 NaN NaN 语音类 2019-02-08 20:51:07 2019 2 用户来电询问车辆怎么开启前风窗除雾功能,已告知 NaN NaN 广东省 朗逸 全新朗逸1.6L 手自一体 舒适版 182DP1 没有回答 2015-03-11 FN024998 21302.0 NaN NaN NaN NaN 7515 NaN NaN NaN 7515 NaN NaT 2019-02-08 20:51:14 N N N N N 客服中心-CC 7515 2019-02-08 20:48:04 NaN 7515 7515 N NaN NaN NaN N N NaN NaN
4 1-8LK-52825 大众 ECS20200405083106-15365279484 NaN 处理完成 咨询 分配 主动来电 维修保养相关 NaN NaN 语音类 2020-04-05 08:32:09 2020 4 来电人姓名:郑先生\n车型:新途观L\n关键字:疫情期间\n来电问题:咨询给自己途观L车辆钥... NaN NaN 湖北省 全新途观L 全新途观L 380TSI (2.0T 162kW) DQ500 豪华版 4MOTION 0T16WT NaN 2017-12-04 H2186140 0.0 NaN NaN NaN NaN 7519 NaN NaN NaN 7519 NaN NaT 2020-04-05 08:32:18 N N N N N 客服中心-CC 7519 2020-04-05 08:28:51 NaN 7519 7519 N NaN NaN NaN N N NaN NaN

Descriptive Analysis(描述性分析)

先看看数据有多少行列吧

In [7]:
print('关键客户基本信息表的行列为:',df_key_customer_info.shape)
print('关键客户重购表的行列为:',df_repurchased.shape)
print('关键客户服务请求表的行列为:',df_service_request.shape)
关键客户基本信息表的行列为: (3955, 27)
关键客户重购表的行列为: (7978, 11)
关键客户服务请求表的行列为: (1447, 57)

再看看有没有重复的行

In [8]:
df_key_customer_info.drop_duplicates()
df_key_customer_info.shape
Out[8]:
(3955, 27)
In [9]:
df_repurchased.drop_duplicates()
df_repurchased.shape
Out[9]:
(7978, 11)
In [10]:
df_service_request.drop_duplicates()
df_service_request.shape
Out[10]:
(1447, 57)

非常好!我们没有重复的行存在。那再看看数据的基本信息吧!

Customer Profile Insights

In [11]:
df_key_customer_info.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3955 entries, 0 to 3954
Data columns (total 27 columns):
 #   Column    Non-Null Count  Dtype         
---  ------    --------------  -----         
 0   车主id      3955 non-null   object        
 1   姓名        3955 non-null   object        
 2   姓名拼音      3441 non-null   object        
 3   英文名称      3955 non-null   object        
 4   性别        3947 non-null   object        
 5   生日        1965 non-null   datetime64[ns]
 6   婚姻状态      0 non-null      float64       
 7   教育程度      2306 non-null   object        
 8   职业大类      0 non-null      float64       
 9   职业小类      0 non-null      float64       
 10  职位        2473 non-null   object        
 11  行业        2152 non-null   object        
 12  兴趣爱好      3 non-null      object        
 13  家庭年收入     1588 non-null   object        
 14  家庭成员人数    1138 non-null   object        
 15  计划考驾照的时间  0 non-null      float64       
 16  是否拥有驾照    1059 non-null   object        
 17  取得驾照年月    2 non-null      datetime64[ns]
 18  是否大客户     30 non-null     object        
 19  是否VIP客户   0 non-null      float64       
 20  手机        3954 non-null   float64       
 21  区号        205 non-null    float64       
 22  分机        2 non-null      float64       
 23  省份        3693 non-null   object        
 24  城市        3611 non-null   object        
 25  址区        3086 non-null   object        
 26  邮编        2647 non-null   object        
dtypes: datetime64[ns](2), float64(8), object(17)
memory usage: 834.4+ KB

通过以上的信息我们可以分析出客户不愿意透露自己的婚姻状况、职业类别、兴趣爱好、驾照相关信息,是否是大/VIP客户。

往往我们可以通过客户的一部分代表性的信息来做一些特征选择,比如我们可对客户基本数据进行总结,大概包括:

  • 客户id
  • 性别
  • 教育程度
  • 职位/行业
  • 收入
  • 地理位置

先去掉用处不大的信息,再进行特征筛选

In [12]:
df_key_customer_info.drop(['姓名拼音',
                           '英文名称',
                           '婚姻状态',
                           '职业大类',
                           '职业小类',
                           '兴趣爱好',
                           '计划考驾照的时间',
                           '取得驾照年月',
                           '是否VIP客户',
                           '区号',
                           '分机'], axis = 1, inplace=True)
In [13]:
df_key_customer_info.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 3955 entries, 0 to 3954
Data columns (total 16 columns):
 #   Column  Non-Null Count  Dtype         
---  ------  --------------  -----         
 0   车主id    3955 non-null   object        
 1   姓名      3955 non-null   object        
 2   性别      3947 non-null   object        
 3   生日      1965 non-null   datetime64[ns]
 4   教育程度    2306 non-null   object        
 5   职位      2473 non-null   object        
 6   行业      2152 non-null   object        
 7   家庭年收入   1588 non-null   object        
 8   家庭成员人数  1138 non-null   object        
 9   是否拥有驾照  1059 non-null   object        
 10  是否大客户   30 non-null     object        
 11  手机      3954 non-null   float64       
 12  省份      3693 non-null   object        
 13  城市      3611 non-null   object        
 14  址区      3086 non-null   object        
 15  邮编      2647 non-null   object        
dtypes: datetime64[ns](1), float64(1), object(14)
memory usage: 494.5+ KB

Data Quality Check

我们先定义一个数据质量检测函数!

In [14]:
def data_check(data, create_new_data = True):
    # 缺失值占比
    print('#### 数据缺失值百分比 ####')
    percent = 100*data.isnull().sum()/ data.shape[0]
    print(percent)
    
    if create_new_data == True:
        # 我们把缺失值小于10%大于0的行去掉
        threshold = 10
        idx = np.where((percent < threshold) & (percent > 0))
        col_drop = pd.DataFrame(percent).T.columns[idx]   
        new_data = data.dropna(axis=0, subset=col_drop)

        print('\n#### 新数据缺失值百分比 ####')
        percent = 100*new_data.isnull().sum()/ new_data.shape[0]
        print(percent)

        return new_data
    
    
df_key_customer_info = data_check(df_key_customer_info)
df_key_customer_info.head()
#### 数据缺失值百分比 ####
车主id       0.000000
姓名         0.000000
性别         0.202276
生日        50.316056
教育程度      41.694058
职位        37.471555
行业        45.587863
家庭年收入     59.848293
家庭成员人数    71.226296
是否拥有驾照    73.223767
是否大客户     99.241466
手机         0.025284
省份         6.624526
城市         8.697851
址区        21.972187
邮编        33.072061
dtype: float64

#### 新数据缺失值百分比 ####
车主id       0.000000
姓名         0.000000
性别         0.000000
生日        47.599223
教育程度      41.659728
职位        37.246739
行业        45.628643
家庭年收入     59.089648
家庭成员人数    72.356370
是否拥有驾照    74.687760
是否大客户     99.361643
手机         0.000000
省份         0.000000
城市         0.000000
址区        14.515681
邮编        29.253400
dtype: float64
Out[14]:
车主id 姓名 性别 生日 教育程度 职位 行业 家庭年收入 家庭成员人数 是否拥有驾照 是否大客户 手机 省份 城市 址区 邮编
0 1-8MN-68814 NaT 初中 总裁/总经理/总监/企业高管 家居、装饰 NaN Four Y NaN 2.717980e+10 北京市 北京市 房山区 102401
1 1-3YPW-6314 1980-02-14 本科 公司拥有者(老板)/合伙人 NaN NaN NaN NaN NaN 2.781596e+10 江西省 景德镇市 珠山区 NaN
3 1-1SB-15357 NaT NaN NaN NaN NaN NaN NaN NaN 2.767710e+10 湖北省 十堰市 NaN 442700
4 1-8LJ-21917 NaT 高中 总裁/总经理/总监/企业高管 电气、电器、仪器制造行业 8-12万元(含12万) No Answer NaN NaN 3.102531e+10 河北省 衡水市 武强县 53000
5 1-8MK-70795 1963-01-22 高中 NaN 电气、电器、仪器制造行业 18-27万元(含27万) NaN NaN NaN 2.703103e+10 安徽省 合肥市 庐阳区 NaN

这里我们有了客户的省份和城市作为地理位置的代表信息,可以将址区和邮编剔除,并且由于是否为大客户的缺失值占比超过99%,也需要把它剔除。

In [15]:
df_key_customer_info.drop(['址区','邮编','是否大客户'],axis = 1,inplace=True)
In [16]:
data_check(df_key_customer_info,create_new_data=False)
#### 数据缺失值百分比 ####
车主id       0.000000
姓名         0.000000
性别         0.000000
生日        47.599223
教育程度      41.659728
职位        37.246739
行业        45.628643
家庭年收入     59.089648
家庭成员人数    72.356370
是否拥有驾照    74.687760
手机         0.000000
省份         0.000000
城市         0.000000
dtype: float64

那么接下来就要对缺失值进行填充,比如我们可以

  • 根据城市的等级以及已有的客户年收入进行对年收入缺失值的填充
  • 对家庭成员人数进行均值填充(成员数也影响了购车习惯和需求)
  • 是否拥有驾照来根据年龄判断进行填充(国内要求18岁以上人群可考驾照,我设定大于30岁的人都拥有驾照)
  • 职位、教育程度、年龄等可以进行众数填充

Data Imputation & Analysis

In [17]:
df_key_customer_info['家庭年收入']
Out[17]:
0                 NaN
1                 NaN
3                 NaN
4        8-12万元(含12万)
5       18-27万元(含27万)
            ...      
3947    12-18万元(含18万)
3948              NaN
3949             没有回答
3951              NaN
3952             没有回答
Name: 家庭年收入, Length: 3603, dtype: object
In [18]:
df_key_customer_info['家庭年收入'].value_counts()
Out[18]:
没有回答             534
8-12万元(含12万)     229
不想说/不愿回答         169
5-8万元(含8万)       166
18-27万元(含27万)    100
12-18万元(含18万)     93
5万元及以下            88
27-45万元(含45万)     53
45万元以上            42
Name: 家庭年收入, dtype: int64
In [19]:
df_key_customer_info['家庭成员人数'].value_counts()
Out[19]:
No Answer          620
Three              269
Four                43
Two                 36
Five                13
One                  8
Six                  6
Seven And Above      1
Name: 家庭成员人数, dtype: int64
In [20]:
df_key_customer_info['家庭成员人数'] = df_key_customer_info['家庭成员人数'].replace(['One','Two','Three','Four','Five','Six','Seven And Above'],
                                                                        [1,2,3,4,5,6,7])
In [21]:
df_key_customer_info['家庭成员人数'] = df_key_customer_info['家庭成员人数'].replace('No Answer',3)
In [22]:
df_key_customer_info['家庭成员人数'] = df_key_customer_info['家庭成员人数'].fillna(method = 'pad')
In [23]:
sns.countplot(df_key_customer_info['家庭成员人数'])
Out[23]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c745da3c08>

可以看出大概90%的客户家庭人数是3人。

再来看看客户的年龄分布

In [24]:
df_key_customer_info['年龄'] = 2020 - df_key_customer_info['生日'].apply(lambda x:x.year) # 对timestamp进行年份转换
In [25]:
df_key_customer_info['年龄'] = df_key_customer_info['年龄'].interpolate() # 对数据缺失值进行插值拟合
In [26]:
plt.figure(figsize = [12,8])
sns.distplot(df_key_customer_info['年龄'])
Out[26]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c745d12cc8>
In [27]:
df_key_customer_info['年龄'][0] = df_key_customer_info['年龄'].mean()
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.

可以看出客户的年龄分布集中在35到50中间,是很合理的,峰值出现在40岁左右

In [28]:
null_idx = df_key_customer_info['是否拥有驾照'][pd.isnull(df_key_customer_info['是否拥有驾照'])].index
df_key_customer_info['是否拥有驾照'][pd.isnull(df_key_customer_info['是否拥有驾照'])] = df_key_customer_info['年龄'][null_idx].apply(lambda x: 'Y' if x > 30 else 'N')
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
In [29]:
df_key_customer_info['是否拥有驾照'].value_counts()
Out[29]:
Y    3308
N     295
Name: 是否拥有驾照, dtype: int64
In [30]:
# 根据年龄对教育程度进行预测(由于75%人不愿回答)
no_answer_idx = df_key_customer_info['教育程度'][df_key_customer_info['教育程度'] == '没有回答'].index

def education(age):
    if (age >= 20) & (age <= 30):
        return '硕士'
    elif (age > 30) & (age <= 43):
        return '本科'
    elif (age > 43) & (age <= 55):
        return '大专'
    else:
        return '高中'
df_key_customer_info['教育程度'][df_key_customer_info['教育程度'] == '没有回答'] = df_key_customer_info['年龄'][no_answer_idx].apply(education)
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:13: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  del sys.path[0]

通过计算,客户的家庭年收入大概在7.6万左右,由于不愿回答或没有回答的人数太多,我们需要进行填充。

In [31]:
df_key_customer_info['教育程度'] = df_key_customer_info['教育程度'].fillna(method = 'pad')
In [32]:
df_key_customer_info['家庭年收入'] = df_key_customer_info['家庭年收入'].replace(['没有回答','不想说/不愿回答','5万元及以下','5-8万元(含8万)', '8-12万元(含12万)','12-18万元(含18万)','18-27万元(含27万)','27-45万元(含45万)','45万元以上'],[65000,100000,50000,65000,100000,150000,230000,360000,500000])
In [33]:
df_key_customer_info['家庭年收入'] = df_key_customer_info['家庭年收入'].fillna(method = 'ffill')
In [34]:
df_key_customer_info['家庭年收入'][0] = 500000
df_key_customer_info['家庭年收入'][1] = 500000
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
In [35]:
df_key_customer_info['职位'] = df_key_customer_info['职位'].fillna(method = 'ffill')
df_key_customer_info['行业'] = df_key_customer_info['行业'].fillna(method = 'ffill')

那么最后整理出来的客户信息数据如下

In [36]:
# 缺失值验证
df_key_customer_info.isnull().sum()
# 去掉不需要的列
col_to_drop = ['姓名','生日','手机']
df_key_customer_info = df_key_customer_info.drop(col_to_drop,axis = 1)
In [37]:
df_key_customer_info = df_key_customer_info.dropna()
df_key_customer_info.isnull().sum()
Out[37]:
车主id      0
性别        0
教育程度      0
职位        0
行业        0
家庭年收入     0
家庭成员人数    0
是否拥有驾照    0
省份        0
城市        0
年龄        0
dtype: int64
In [38]:
df_key_customer_info = df_key_customer_info.rename(columns={'车主id':'车主ID'})
In [39]:
df_key_customer_info.index = df_key_customer_info['车主ID']
In [40]:
df_key_customer_info = df_key_customer_info.drop('车主ID',axis = 1)
print('最终客户数据的维度是:',df_key_customer_info.shape)
df_key_customer_info.head()
最终客户数据的维度是: (3602, 10)
Out[40]:
性别 教育程度 职位 行业 家庭年收入 家庭成员人数 是否拥有驾照 省份 城市 年龄
车主ID
1-8MN-68814 初中 总裁/总经理/总监/企业高管 家居、装饰 500000.0 4.0 Y 北京市 北京市 43.695031
1-3YPW-6314 本科 公司拥有者(老板)/合伙人 家居、装饰 500000.0 4.0 Y 江西省 景德镇市 40.000000
1-8LJ-21917 高中 总裁/总经理/总监/企业高管 电气、电器、仪器制造行业 100000.0 3.0 Y 河北省 衡水市 51.333333
1-8MK-70795 高中 总裁/总经理/总监/企业高管 电气、电器、仪器制造行业 230000.0 3.0 Y 安徽省 合肥市 57.000000
1-3S29-22819 高中 总裁/总经理/总监/企业高管 电气、电器、仪器制造行业 230000.0 3.0 Y 山东省 菏泽市 37.000000

来写一个辅助函数,画出每个特征的饼状图来帮助分析

In [41]:
def mypie(data,feature,top_n = 10):
    plt.figure(figsize=(12,8))
    labels = data[feature].value_counts()[:top_n].index
    plt.pie(data[feature].value_counts()[:top_n],labels = labels,autopct='%3.2f%%')
In [42]:
mypie(df_key_customer_info,'性别')

可以看出客户71.75%集中在男性群体上。

In [43]:
mypie(df_key_customer_info,'教育程度')

根据客户的年龄分布以及饼状图的结果来看,38.11%的客户是本科学历,36.68%的客户是大专学历,11.42%的客户是高中学历。硕士、博士级以上只占了3%左右。

说明客户的教育水平大多是集中在大专和本科,其总占比达到75%左右。

In [44]:
mypie(df_key_customer_info,'职位')

根据上图所示,我们有23.56%的客户不愿意透露自己的职业,而也有24.5%左右的客户是企业高管等高层职位,有21.23%的客户是一般职员或者技术人员,而事业单位或者正负官员占比是最少的,不到1%,所以客户的职位有一些两级分化,总裁等级的职位和一般职员的占比不分伯仲。

In [45]:
mypie(df_key_customer_info,'行业')

根据饼状图,33.85%的客户都集中在电气、电源、仪器制造行业。而其他行业。例如:家居、建筑、汽车、政府、金融、教育等,比重都差距不大,其中也有23%的人拒绝回答自己从事的行业。

In [46]:
mypie(df_key_customer_info,'省份')

客户在上海市(直辖市)的总数量是最高的,达到15.96%,但是也和江苏省的14.96%、山东省的14.72%相差不多,山西省占比5%是最少的,在南北地区分布来看,客户分布数量基本持平。

In [47]:
mypie(df_key_customer_info,'城市')

大部分的客户都分布在上海市和北京市,总占比达到50%左右。其中上海市占比最高,达到34.36%,北京市排在第二,达到了15.29%。

In [48]:
plt.figure(figsize=(12,8))
labels = df_key_customer_info['家庭年收入'].value_counts().index
plt.pie(df_key_customer_info['家庭年收入'].value_counts(),labels = labels,autopct='%3.2f%%')
Out[48]:
([<matplotlib.patches.Wedge at 0x1c7456b5ec8>,
  <matplotlib.patches.Wedge at 0x1c7456b8b48>,
  <matplotlib.patches.Wedge at 0x1c7456b9b88>,
  <matplotlib.patches.Wedge at 0x1c7456bcd88>,
  <matplotlib.patches.Wedge at 0x1c7456bd908>,
  <matplotlib.patches.Wedge at 0x1c7456bf048>,
  <matplotlib.patches.Wedge at 0x1c7456c0e88>],
 [Text(0.08721364286633324, 1.0965371769794146, '65000.0'),
  Text(-0.8317242959725909, -0.7198851960478825, '100000.0'),
  Text(0.2426344554927214, -1.0729065760856118, '230000.0'),
  Text(0.6400265190009816, -0.8946317985492614, '50000.0'),
  Text(0.9185284936327616, -0.6052316964475093, '150000.0'),
  Text(1.0505763955383058, -0.32602030172635155, '360000.0'),
  Text(1.0947559438885213, -0.10728198041028623, '500000.0')],
 [Text(0.04757107792709085, 0.5981111874433169, '47.47%'),
  Text(-0.45366779780323135, -0.392664652389754, '27.76%'),
  Text(0.13234606663239346, -0.58522176877397, '6.61%'),
  Text(0.34910537400053543, -0.4879809810268698, '6.08%'),
  Text(0.5010155419815062, -0.3301263798804596, '5.61%'),
  Text(0.5730416702936213, -0.17782925548710082, '3.36%'),
  Text(0.5971396057573751, -0.05851744386015612, '3.11%')])

47.5%的客户家庭年收入水平在65000元左右,27.7%的客户家庭年收入在8-12万元,并且也造成了一些两极分化的情况,年收入5万元以下和年收入在12-18万或18-27万的客户基本持平,都在6%左右。也说明年收入哪怕在5万元以下的情况,对汽车的需求也是比较高的,而我们的客户群体不集中与高收入人群,普遍集中在中等偏低收入水平的客户,总共占比大概在86%左右(针对5-15万年收入群体)。

Summary

客户(车主)基本数据囊括了其个人基本信息、教育程度、职业行业、收入水平以及地理分布。我通过对数据的质检以及描述性分析,对客户数据的各个维度都进行了探索性分析以及数据可视化,最终,我得出的结论如下:

  • 71%的客户为男性客户,说明男性在购车方面占据主导。
  • 客户的年龄分布集中在35到50中间,峰值出现在40岁左右,说明中年人的购车欲望和需求是最高的。
  • 客户的教育水平集中在大专和本科,其总占比达到75%左右,而硕士、博士及以上占比不到3%,说明客户的购车需求并不和学历产生正向关系,学历中等偏上的人群占据主导。
  • 63%客户的家庭年收入在12万以下(工资水平偏低)占主导地位,25%客户的家庭收入在12万到27万(工资水平中等),12%客户的家庭收入在27万以上。
  • 客户的职业大部分集中在总裁、总经理等高层职业(24.1%)和技术员工等一般职业(21.8%),总占比达到55%,而政府官员占比最少,不到1%,说明队友客户偏好上来说,我们更应大量发展高端昂贵车辆(适合高层职位)和经济实用型车辆(适合普通员工)的相关业务。
  • 客户工作的行业有38%集中在电气、电器、仪器制造行业,而其他行业相对分布均衡。
  • 客户地理位置分布主要在上海和北京,并且总体来说南北地区客户数量相持平。

Repurchase Related Insights

先来看看客户回购数据的基本信息

In [49]:
df_repurchased.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7978 entries, 0 to 7977
Data columns (total 11 columns):
 #   Column          Non-Null Count  Dtype         
---  ------          --------------  -----         
 0   车主ID            7978 non-null   object        
 1   VVIN            7800 non-null   object        
 2   市场车型            7938 non-null   object        
 3   车型              7938 non-null   object        
 4   购买日期            7958 non-null   datetime64[ns]
 5   开票价格            7978 non-null   float64       
 6   ASSET_REF_EXPR  7938 non-null   object        
 7   OU_NAME         7464 non-null   object        
 8   OU_ABREV        7464 non-null   object        
 9   OU_CITY         7460 non-null   object        
 10  OU_COUNTY       7238 non-null   object        
dtypes: datetime64[ns](1), float64(1), object(9)
memory usage: 685.7+ KB

可以看出基本没有什么缺失值,尤其是车型、购买日期和开票价格。

In [50]:
df_repurchased = data_check(df_repurchased,create_new_data=True)
#### 数据缺失值百分比 ####
车主ID              0.000000
VVIN              2.231136
市场车型              0.501379
车型                0.501379
购买日期              0.250689
开票价格              0.000000
ASSET_REF_EXPR    0.501379
OU_NAME           6.442717
OU_ABREV          6.442717
OU_CITY           6.492855
OU_COUNTY         9.275508
dtype: float64

#### 新数据缺失值百分比 ####
车主ID              0.0
VVIN              0.0
市场车型              0.0
车型                0.0
购买日期              0.0
开票价格              0.0
ASSET_REF_EXPR    0.0
OU_NAME           0.0
OU_ABREV          0.0
OU_CITY           0.0
OU_COUNTY         0.0
dtype: float64

这样我们就把缺失值少于10%的所有行全部去掉。

In [51]:
df_repurchased = df_repurchased.drop_duplicates()
In [52]:
df_repurchased.shape
Out[52]:
(7233, 11)

找出没有重新购买的所有顾客和其信息

In [53]:
d = pd.DataFrame(df_repurchased['车主ID'].value_counts())
idx = d[d['车主ID']==1].index
df_repurchased_test = pd.DataFrame(columns=df_repurchased.columns)
for i in range(len(idx)):
    df_repurchased_test = df_repurchased_test.append(df_repurchased[df_repurchased['车主ID'] == idx[i]])
In [54]:
df_repurchased_test
Out[54]:
车主ID VVIN 市场车型 车型 购买日期 开票价格 ASSET_REF_EXPR OU_NAME OU_ABREV OU_CITY OU_COUNTY
2389 1-8I8-3481 92732423 波罗劲情 VY992-波罗 劲情 1.4L 手动 时尚版 OBD 选装铝轮毂 2010-01-15 133000.0 大众 开封金源汽车销售服务有限公司 河南省 开封市 金明区
3906 1-8FP-79638 82000525 波罗劲取 VT702-波罗 劲取 1.6L 手动 雅致版 2008-01-09 50000.0 大众 河南裕华上联汽车销售服务有限公司 河南省 郑州市 管城回族区
3265 1-8NM-40997 D2024271 晶锐 新晶锐1.4L手动晶灵版 2013-07-01 127800.0 斯柯达 晋城市唐辉汽贸有限公司 山西省 晋城市 晋城城区
5223 1-8EK-29544 D2046669 途观 VR9E3-途观 1.8T 自动 前驱 风尚版 黑色真皮C5 MP12 2013-05-16 283800.0 大众 浙江众美汽车有限公司 浙江省 杭州市 余杭区
1145 1-8EK-22485 E2098562 全新途观 全新途观 2.0T 自动 4驱 豪华版 2014-07-01 342950.0 大众 余姚申浙汽车有限责任公司 浙江省 宁波市 余姚市
... ... ... ... ... ... ... ... ... ... ... ...
2400 1-8EI-99402 72270481 帕萨特领驭 VC192-帕萨特领驭经典型 2.0L 手动 标准型(带PDC及真皮) 2007-04-12 50000.0 大众 河北众诚汽车贸易有限公司 河北省 石家庄市 石家庄长安区
3409 1-8GZ-70998 92194041 朗逸 VS162-朗逸 1.6L 自动 品雅 2009-02-28 179600.0 大众 揭阳市群记汽车贸易有限公司 广东省 揭阳市 揭东县
4157 1-1S6-50136 42137407 桑塔纳Vista VL122-桑塔纳3000 1.8L手动舒适型 2004-07-01 50000.0 大众 驻马店市天成汽车销售服务有限公司 河南省 驻马店市 驿城区
6545 1-8FO-54354 72121392 桑塔纳Vista VQBD2-桑塔纳3000 2.0L 自动 豪华型(E3) 2007-06-27 50000.0 大众 上海百联沪东汽车销售服务有限公司 上海市 上海市 闸北区
2304 1-8EP-84230 22239895 桑塔纳 VN422-桑塔纳1.8L手动豪华型II型 2002-12-27 50000.0 大众 瓦房店市顺发物资商贸有限公司 辽宁省 大连市 瓦房店市

486 rows × 11 columns

可以看出我们有486行数据代表没有重购的情况,我们将把他们作为分析的对象。

In [55]:
d = pd.DataFrame(df_repurchased['车主ID'].value_counts())
idx = d[d['车主ID'] > 1].index
df_repurchased_train = pd.DataFrame(columns=df_repurchased.columns)
for i in range(len(idx)):
    df_repurchased_train = df_repurchased_train.append(df_repurchased[df_repurchased['车主ID'] == idx[i]])
In [56]:
df_repurchased_train
Out[56]:
车主ID VVIN 市场车型 车型 购买日期 开票价格 ASSET_REF_EXPR OU_NAME OU_ABREV OU_CITY OU_COUNTY
598 1-8KF-23097 JN039263 途安L 全新途安 L280 TSI (1.4TSI) DQ200 拓界版 2018-11-01 50001.0 大众 呼和浩特市坤德汽车销售有限公司 内蒙古自治区 呼和浩特市 赛罕区
1585 1-8KF-23097 B2733645 新波罗 VHF12-新波罗 1.6L 自动 致尚版(黑色织物) 2012-03-09 156700.0 大众 内蒙古利丰泰裕汽车服务有限公司 内蒙古自治区 呼和浩特市 回民区
2580 1-8KF-23097 JN039263 途安L 全新途安 L280 TSI (1.4TSI) DQ200 拓界版 2018-11-01 192800.0 大众 呼和浩特市坤德汽车销售有限公司 内蒙古自治区 呼和浩特市 赛罕区
1002 1-1EMM-12414 DN196624 全新一代桑塔纳 全新一代桑塔纳 1.6L自动 舒适版 2014-01-22 159800.0 大众 株洲益源汽车销售服务有限公司 湖南省 株洲市 石峰区
6959 1-1EMM-12414 K2268823 全新一代朗逸 全新一代朗逸 1.5L 自动视野版 2020-04-23 148900.0 大众 株洲益源汽车销售服务有限公司 湖南省 株洲市 石峰区
... ... ... ... ... ... ... ... ... ... ... ...
7676 1-Z0E-2495 DN121857 全新朗逸 VSEP2-全新朗逸1.6L C4 手自一体 舒适版 米色 2013-12-06 181900.0 大众 南京致远汽车销售服务有限公司 江苏省 南京市 江宁区
483 1-8ML-94736 C2012037 朗逸 VS962-朗逸 1.6L 手自一体 品悠 2012-01-19 171300.0 大众 徐州恒运汽车销售服务有限公司 江苏省 徐州市 云龙区
484 1-8ML-94736 C2115142 明锐 明锐经典款1.6L手动逸杰版 2012-11-18 165900.0 斯柯达 连云港市翔盛方通汽车销售有限公司 江苏省 连云港市 新浦区
4271 1-8EI-29764 82542276 帕萨特领驭 VC602-帕萨特领驭经典型 1.8T 手动 MP07 标准型 2010-01-01 228800.0 大众 青岛顺驰汽车销售有限公司 山东省 青岛市 胶南市
7255 1-8EI-29764 42348116 桑塔纳B2 VN581-桑塔纳B2 1.8L手动 03款 豪华型 2004-05-26 50000.0 大众 青岛瑞源宏业汽车销售服务有限公司 山东省 青岛市 黄岛区

6747 rows × 11 columns

剩下的6747行数据作为我们的分析训练数据,我想通过分析这些客户的回购数据来为还未重购的客户进行推荐!

In [57]:
df_repurchased_train.sort_values(by = ['车主ID','购买日期'])
Out[57]:
车主ID VVIN 市场车型 车型 购买日期 开票价格 ASSET_REF_EXPR OU_NAME OU_ABREV OU_CITY OU_COUNTY
5710 1-10LD-2240 DN202047 全新帕萨特 VFBM1-全新帕萨特 1.8T DSG 尊荣版(黑色)C5 2013-12-10 260800.0 大众 沧州运鹏汽车销售服务有限公司 河北省 沧州市 沧州新华区
1760 1-10LD-2240 DN179100 全新帕萨特 VFBP1-全新帕萨特 1.8T DSG 政府采购车 黑色C5 2013-12-16 252400.0 大众 沧州兴华汽车销售服务有限公司 河北省 沧州市 运河区
4049 1-10ZA-842 D2019597 新途安 VEBA3-途安2013款 1.4T DSG 舒适版 5座 黑色C4 2013-12-12 213800.0 大众 南京朗驰集团沪宁汽车销售服务有限公司 江苏省 南京市 白下区
4050 1-10ZA-842 FN038115 全新朗逸 182DP1-全新朗逸1.6L 手自一体 舒适版 2015-05-06 50000.0 大众 南京公用发展股份有限公司 江苏省 南京市 建邺区
115 1-11GN3TJ GN031260 全新一代桑塔纳 全新一代桑塔纳 1.6L手动 风尚版 2016-02-29 120500.0 大众 宿州海昌汽车销售服务有限责任公司 安徽省 宿州市 埇桥区
... ... ... ... ... ... ... ... ... ... ... ...
5152 1-ZPOITB GN066093 全新一代桑塔纳 全新一代桑塔纳 1.6L手动 风尚版 2016-03-31 116900.0 大众 保定市聚合汽车维修有限公司 河北省 保定市 保定新市区
4971 1-ZUL-1836 DN038921 2017款昕锐 昕锐 1.6L 自动 智选版 2013-12-05 144700.0 斯柯达 成都国跃车业有限公司 四川省 成都市 武侯区
2994 1-ZUL-1836 H2088908 全新途观 途观丝绸之路版 280TSI DQ250 舒适版 2017-10-08 239800.0 大众 四川广博汽车有限公司 四川省 成都市 武侯区
3762 1-ZUZ-86 D2113764 明锐 2014款明锐 1.6L 手自动一体 逸俊版(铝制发动机) 2013-12-05 184600.0 斯柯达 陕西嘉盛机电有限公司 陕西省 西安市 未央区
2739 1-ZUZ-86 D2047112 速派 速派 1.8TSI 手自动一体 名仕版 2014-05-26 211160.0 斯柯达 陕西锐风汽车销售服务有限公司 陕西省 西安市 未央区

6747 rows × 11 columns

In [58]:
df_repurchased_train['市场车型'].describe()
Out[58]:
count      6747
unique       76
top       全新帕萨特
freq        663
Name: 市场车型, dtype: object
In [59]:
percent = (100*df_repurchased_train['市场车型'].value_counts()/df_repurchased_train.shape[0])[:30].sum()
print('我们要选取最受客户欢迎的前30种车,对于总数的占比达到:',percent)
我们要选取最受客户欢迎的前30种车,对于总数的占比达到: 86.76448792055729
In [60]:
idx = pd.DataFrame((100*df_repurchased_train['市场车型'].value_counts()/df_repurchased_train.shape[0])[:30]).index
df_repurchased_train_filter = pd.DataFrame(columns=df_repurchased.columns)
for i in range(len(idx)):
    df_repurchased_train_filter = df_repurchased_train_filter.append(df_repurchased_train[df_repurchased_train['市场车型'] == idx[i]])
In [61]:
df_repurchased_train_filter['市场车型'].value_counts()
Out[61]:
全新帕萨特            663
全新途观             460
新波罗              451
桑塔纳Vista         357
明锐               345
桑塔纳B2            341
全新途观L            317
朗逸               314
全新一代桑塔纳          245
全新朗逸             205
途观               189
New Lavida FL    172
晶锐               166
帕萨特领驭            163
新桑塔纳             140
帕萨特新领驭           128
波罗劲情             128
途昂               125
凌渡               103
速派                99
新途安               97
波罗劲取              87
全新一代帕萨特           79
柯迪亚克              77
2017款明锐           77
途岳                70
途安L               68
2017款昕锐           67
全新速派              63
全新一代朗逸            58
Name: 市场车型, dtype: int64
In [62]:
mypie(df_repurchased_train_filter,'市场车型',top_n = 30)

可以看出占主导地位的车型是全新帕萨特(11.33%)

In [63]:
df_repurchased_train_filter = df_repurchased_train_filter.sort_values(by = ['车主ID','购买日期'])
df_repurchased_train_filter
Out[63]:
车主ID VVIN 市场车型 车型 购买日期 开票价格 ASSET_REF_EXPR OU_NAME OU_ABREV OU_CITY OU_COUNTY
5710 1-10LD-2240 DN202047 全新帕萨特 VFBM1-全新帕萨特 1.8T DSG 尊荣版(黑色)C5 2013-12-10 260800.0 大众 沧州运鹏汽车销售服务有限公司 河北省 沧州市 沧州新华区
1760 1-10LD-2240 DN179100 全新帕萨特 VFBP1-全新帕萨特 1.8T DSG 政府采购车 黑色C5 2013-12-16 252400.0 大众 沧州兴华汽车销售服务有限公司 河北省 沧州市 运河区
4049 1-10ZA-842 D2019597 新途安 VEBA3-途安2013款 1.4T DSG 舒适版 5座 黑色C4 2013-12-12 213800.0 大众 南京朗驰集团沪宁汽车销售服务有限公司 江苏省 南京市 白下区
4050 1-10ZA-842 FN038115 全新朗逸 182DP1-全新朗逸1.6L 手自一体 舒适版 2015-05-06 50000.0 大众 南京公用发展股份有限公司 江苏省 南京市 建邺区
115 1-11GN3TJ GN031260 全新一代桑塔纳 全新一代桑塔纳 1.6L手动 风尚版 2016-02-29 120500.0 大众 宿州海昌汽车销售服务有限责任公司 安徽省 宿州市 埇桥区
... ... ... ... ... ... ... ... ... ... ... ...
5152 1-ZPOITB GN066093 全新一代桑塔纳 全新一代桑塔纳 1.6L手动 风尚版 2016-03-31 116900.0 大众 保定市聚合汽车维修有限公司 河北省 保定市 保定新市区
4971 1-ZUL-1836 DN038921 2017款昕锐 昕锐 1.6L 自动 智选版 2013-12-05 144700.0 斯柯达 成都国跃车业有限公司 四川省 成都市 武侯区
2994 1-ZUL-1836 H2088908 全新途观 途观丝绸之路版 280TSI DQ250 舒适版 2017-10-08 239800.0 大众 四川广博汽车有限公司 四川省 成都市 武侯区
3762 1-ZUZ-86 D2113764 明锐 2014款明锐 1.6L 手自动一体 逸俊版(铝制发动机) 2013-12-05 184600.0 斯柯达 陕西嘉盛机电有限公司 陕西省 西安市 未央区
2739 1-ZUZ-86 D2047112 速派 速派 1.8TSI 手自动一体 名仕版 2014-05-26 211160.0 斯柯达 陕西锐风汽车销售服务有限公司 陕西省 西安市 未央区

5854 rows × 11 columns

In [64]:
df = df_repurchased_train
In [65]:
df = df.sort_values(by = ['车主ID','购买日期'])
In [66]:
df['价格差异'] = df['开票价格'].groupby(df['车主ID']).diff()
In [67]:
plt.figure(figsize = [12,8])
df_price_diff = pd.DataFrame(df['价格差异'].groupby(df['车主ID']).sum())
sns.distplot(df_price_diff)
Out[67]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c744c97948>

通过对于重购客户的回购车辆价格和首次购车价格的差值分布情况来看,绝大部分的客户在购车的价格上是基本持平或者会倾向于买稍微贵一点的车。

In [68]:
df['开票价格'].max()
Out[68]:
2272900.0
In [69]:
df_price_diff.describe()
Out[69]:
价格差异
count 3.343000e+03
mean 5.846422e+04
std 1.146036e+05
min -4.815000e+05
25% 0.000000e+00
50% 4.980000e+04
75% 1.265000e+05
max 2.222900e+06

可以看出平均来讲,重购车辆的价格会比之前高出46000元左右,中位数大概在7000元左右,最大价格差要高出了220多万,最少比之前低了48万多,所以这个差异变动还是比较大的!

再看一下他们的购车时间间隔的情况

In [70]:
df['时间差异'] = df['购买日期'].groupby(df['车主ID']).diff()
In [71]:
plt.figure(figsize = [12,8])
df_time_diff = pd.DataFrame(df['时间差异'].groupby(df['车主ID']).sum())
df_time_diff['时间差异'] = df_time_diff['时间差异'].apply(lambda x: x.days)
sns.distplot(df_time_diff['时间差异'])
Out[71]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c745c7c148>

我们放大一下

In [72]:
df_time_diff['时间差异'].describe()
Out[72]:
count     3343.000000
mean      1691.990129
std       3278.085190
min          0.000000
25%        519.500000
50%       1329.000000
75%       2175.000000
max      43694.000000
Name: 时间差异, dtype: float64
In [73]:
plt.figure(figsize = [12,8])
sns.distplot(df_time_diff['时间差异'][df_time_diff['时间差异']<2175])
Out[73]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c7454f2f88>

可以看出,非常奇怪的是有不少客户在买了第一辆车没过几天就重购了,甚至有

In [74]:
print('当天重购的比率是:',100*sum(df_time_diff['时间差异']==0)/df_time_diff.shape[0])
print('当月重购的比率是:',100*sum((df_time_diff['时间差异']>0) & (df_time_diff['时间差异']<=30))/df_time_diff.shape[0])
print('当季重购的比率是:',100*sum((df_time_diff['时间差异']>0) & (df_time_diff['时间差异']<=120))/df_time_diff.shape[0])
print('当年重购的比率是:',100*sum((df_time_diff['时间差异']>0) & (df_time_diff['时间差异']<=365))/df_time_diff.shape[0])
print('第二年重购的比率是:',100*sum((df_time_diff['时间差异']>365) & (df_time_diff['时间差异']<=365*2))/df_time_diff.shape[0])
print('第三年重购的比率是:',100*sum((df_time_diff['时间差异']>365*2) & (df_time_diff['时间差异']<=365*3))/df_time_diff.shape[0])
print('第四年重购的比率是:',100*sum((df_time_diff['时间差异']>365*3) & (df_time_diff['时间差异']<=365*4))/df_time_diff.shape[0])
print('第五年重购的比率是:',100*sum((df_time_diff['时间差异']>365*4) & (df_time_diff['时间差异']<=365*5))/df_time_diff.shape[0])
print('第六年重购的比率是:',100*sum((df_time_diff['时间差异']>365*5) & (df_time_diff['时间差异']<=365*6))/df_time_diff.shape[0])
print('第七年重购的比率是:',100*sum((df_time_diff['时间差异']>365*6) & (df_time_diff['时间差异']<=365*7))/df_time_diff.shape[0])
print('第八年重购的比率是:',100*sum((df_time_diff['时间差异']>365*7) & (df_time_diff['时间差异']<=365*8))/df_time_diff.shape[0])
print('第九年重购的比率是:',100*sum((df_time_diff['时间差异']>365*8) & (df_time_diff['时间差异']<=365*9))/df_time_diff.shape[0])
print('第十年重购的比率是:',100*sum((df_time_diff['时间差异']>365*9) & (df_time_diff['时间差异']<=365*10))/df_time_diff.shape[0])
当天重购的比率是: 4.367334729285074
当月重购的比率是: 4.217768471432845
当季重购的比率是: 8.076577924020341
当年重购的比率是: 15.973676338618008
第二年重购的比率是: 11.097816332635357
第三年重购的比率是: 11.307209093628478
第四年重购的比率是: 11.696081364044272
第五年重购的比率是: 11.037989829494466
第六年重购的比率是: 9.931199521387974
第七年重购的比率是: 7.508226144181872
第八年重购的比率是: 5.50403828896201
第九年重购的比率是: 4.277594974573736
第十年重购的比率是: 2.9015854023332337
In [75]:
y = [4.37, 4.21, 8.08, 15.97, 11.10, 11.31, 11.70, 11.04, 9.93, 7.51, 5.50, 4.28, 2.90]
x = [0,1/12,1/4,1,2,3,4,5,6,7,8,9,10]
plt.figure(figsize = [12,8])
plt.plot(x,y,c='red')
plt.xlabel('第n年重购')
plt.ylabel('百分比')

x_major_locator=MultipleLocator(1)
y_major_locator=MultipleLocator(1)
ax=plt.gca()
ax.xaxis.set_major_locator(x_major_locator)
ax.yaxis.set_major_locator(y_major_locator)
plt.xlim(-0.1,10)
#把x轴的刻度范围设置为-0.5到11,因为0.5不满一个刻度间隔,所以数字不会显示出来,但是能看到一点空白
plt.ylim(-0.1,20)
Out[75]:
(-0.1, 20)

如图所示,我发现在当天重购和当月重购的比率是很接近的,都是4%左右,而大部分的客户选择在第一年内重购(占比16%),在第二、三、四、五年里重购的比重也十分相似,都在11%左右,而随着时间的推移,重购比率也是一直在下降,选择第十年重购的客户仅仅占2%左右。

接下来,我要判断客户的积极程度(高中低)时间区间 消费水平(高中低)消费区间 把车分档次(按卖出的百分比最后进行推荐)制作class 合并之前表格 选出特征 带入xgboost建模

现在我们要刻画用户的积极程度:

  • 重购时间差小于1年 -- 1级
  • 重购时间差大于1年小于3年 -- 2级
  • 重购时间差大于3年小于5年 -- 3级
  • 重购时间差大于5年小于7年 -- 4级
  • 重购时间差大于7年小于9年 -- 5级
  • 重购时间差大于9年 -- 6级
In [76]:
def active(x):
    if x <= 365:
        return 1
    elif (x > 365) & (x <= 365*3):
        return 2
    elif (x > 365*3) & (x <= 365*5):
        return 3
    elif (x > 365*5) & (x <= 365*7):
        return 4
    elif (x > 365*7) & (x <= 365*9):
        return 5
    elif x > 365*9:
        return 6
    
    
df_time_diff['积极程度'] = df_time_diff['时间差异'].apply(lambda x: active(x))
In [77]:
mypie(df_time_diff, '积极程度')

再根据购车价格把市场车型进行分类,根据价格区间把对应车型号汇总。

In [78]:
plt.figure(figsize = [12,8])
df_car_price = df['开票价格'].groupby(df['市场车型']).mean().sort_values()
df_car_price.plot()

x_major_locator=MultipleLocator(10)
y_major_locator=MultipleLocator(50000)
ax=plt.gca()
ax.xaxis.set_major_locator(x_major_locator)
ax.yaxis.set_major_locator(y_major_locator)
plt.xlim(-1,76)
#把x轴的刻度范围设置为-0.5到11,因为0.5不满一个刻度间隔,所以数字不会显示出来,但是能看到一点空白
plt.ylim(-1,700000)
plt.title('不同车型的价格曲线')
Out[78]:
Text(0.5, 1.0, '不同车型的价格曲线')
In [79]:
df_car_price[(df_car_price >= 50000)&(df_car_price < 100000)].count()
Out[79]:
11
In [80]:
df_car_price.describe()
Out[80]:
count        76.000000
mean     182780.857172
std       95884.115413
min       50000.000000
25%      125316.218954
50%      165175.000000
75%      225717.232684
max      639000.000000
Name: 开票价格, dtype: float64

可以看出,我们一共有76种车型,平均价格在18万3千元左右。

最贵的车款是途锐,均价在639000元左右,我把30-60万以内的车款有7种,定为6级; 20-30万以内的车款有20种,定义为4级; 10-20万以内的车款有38种,定义为2级; 5-10万以内的车款有12种,定义为0级。

所以针对不同类型的客户,我们根据其基本信息、购买力、消费意愿以及积极程度来预测他们的回购情况,并相应的推荐车款。

比如一位客户在分析过后应该为其选择在第3级的车款,也就是在20万到30万之间,但是里面有20种车款供选择,我认为可以根据每款车的销量在其中占比来划分权重,给到客户一个比率,并给出车款之间的比较数据供其参考。

In [81]:
pd.DataFrame(df_car_price)
Out[81]:
开票价格
市场车型
高尔 50000.000000
桑塔纳2000 50000.000000
帕萨特 50000.000000
波罗 50000.000000
波罗三厢 50000.000000
... ...
夏朗 327241.000000
辉昂 336663.428571
Teramont X 348092.307692
途昂 388962.520000
途锐 639000.000000

76 rows × 1 columns

In [82]:
def price_level(x):
    if x >= 300000:
        return 6
    elif (x>=200000) & (x<300000):
        return 4
    elif (x>=100000) & (x<200000):
        return 2
    elif x <100000:
        return 0
    
df['车款评级'] = df['开票价格'].apply(lambda x: price_level(x))
df.head()
Out[82]:
车主ID VVIN 市场车型 车型 购买日期 开票价格 ASSET_REF_EXPR OU_NAME OU_ABREV OU_CITY OU_COUNTY 价格差异 时间差异 车款评级
5710 1-10LD-2240 DN202047 全新帕萨特 VFBM1-全新帕萨特 1.8T DSG 尊荣版(黑色)C5 2013-12-10 260800.0 大众 沧州运鹏汽车销售服务有限公司 河北省 沧州市 沧州新华区 NaN NaT 4
1760 1-10LD-2240 DN179100 全新帕萨特 VFBP1-全新帕萨特 1.8T DSG 政府采购车 黑色C5 2013-12-16 252400.0 大众 沧州兴华汽车销售服务有限公司 河北省 沧州市 运河区 -8400.0 6 days 4
4049 1-10ZA-842 D2019597 新途安 VEBA3-途安2013款 1.4T DSG 舒适版 5座 黑色C4 2013-12-12 213800.0 大众 南京朗驰集团沪宁汽车销售服务有限公司 江苏省 南京市 白下区 NaN NaT 4
4050 1-10ZA-842 FN038115 全新朗逸 182DP1-全新朗逸1.6L 手自一体 舒适版 2015-05-06 50000.0 大众 南京公用发展股份有限公司 江苏省 南京市 建邺区 -163800.0 510 days 0
115 1-11GN3TJ GN031260 全新一代桑塔纳 全新一代桑塔纳 1.6L手动 风尚版 2016-02-29 120500.0 大众 宿州海昌汽车销售服务有限责任公司 安徽省 宿州市 埇桥区 NaN NaT 2
In [83]:
df['时间差异'] = df['时间差异'].apply(lambda x: x.days)
In [84]:
col_to_use = ['车主ID', '市场车型', '开票价格','购买日期', 'ASSET_REF_EXPR', 'OU_ABREV', 'OU_NAME','价格差异', '时间差异', '车款评级']
df[col_to_use].groupby(df['车主ID']).mean()
Out[84]:
开票价格 价格差异 时间差异 车款评级
车主ID
1-10LD-2240 256600.0 -8400.0 6.0 4.0
1-10ZA-842 131900.0 -163800.0 510.0 2.0
1-11GN3TJ 119650.0 -1700.0 495.0 2.0
1-11GNTKY 191850.0 -143900.0 3.0 3.0
1-11HMNQQ 323850.0 63900.0 493.0 5.0
... ... ... ... ...
1-Z7U-601 187950.0 24100.0 947.0 3.0
1-ZG3W1L 224900.0 76000.0 716.0 3.0
1-ZPOITB 114900.0 4000.0 39.0 2.0
1-ZUL-1836 192250.0 95100.0 1403.0 3.0
1-ZUZ-86 197880.0 26560.0 172.0 3.0

3343 rows × 4 columns

In [85]:
df['ASSET_REF_EXPR'].value_counts()
Out[85]:
大众     5647
斯柯达    1100
Name: ASSET_REF_EXPR, dtype: int64
In [86]:
d = df.dropna()
train = d[col_to_use].groupby(d['车主ID']).max()
train = train.drop('车主ID',axis = 1)
In [87]:
train['购买年份'] = train['购买日期'].apply(lambda x: x.year)
train['购买月份'] = train['购买日期'].apply(lambda x: x.month)
train['购买日'] = train['购买日期'].apply(lambda x: x.day)
train = train.drop('购买日期',axis = 1)
In [88]:
train = train.rename(columns={'ASSET_REF_EXPR':'品牌',
                      'OU_ABREV':'所购买省份',
                      'OU_NAME':'所购买公司'})
train.head()
Out[88]:
市场车型 开票价格 品牌 所购买省份 所购买公司 价格差异 时间差异 车款评级 购买年份 购买月份 购买日
车主ID
1-10LD-2240 全新帕萨特 252400.0 大众 河北省 沧州兴华汽车销售服务有限公司 -8400.0 6.0 4 2013 12 16
1-10ZA-842 全新朗逸 50000.0 大众 江苏省 南京公用发展股份有限公司 -163800.0 510.0 0 2015 5 6
1-11GN3TJ 新桑塔纳 118800.0 大众 安徽省 宿州海昌汽车销售服务有限责任公司 -1700.0 495.0 2 2017 7 8
1-11GNTKY 全新一代桑塔纳 119900.0 大众 北京市 北京市艾潇汽车有限公司 -143900.0 3.0 2 2016 3 3
1-11HMNQQ 全新途观L 355800.0 大众 贵州省 贵州华通众源汽车贸易服务有限公司 63900.0 493.0 6 2017 7 7
In [89]:
train
Out[89]:
市场车型 开票价格 品牌 所购买省份 所购买公司 价格差异 时间差异 车款评级 购买年份 购买月份 购买日
车主ID
1-10LD-2240 全新帕萨特 252400.0 大众 河北省 沧州兴华汽车销售服务有限公司 -8400.0 6.0 4 2013 12 16
1-10ZA-842 全新朗逸 50000.0 大众 江苏省 南京公用发展股份有限公司 -163800.0 510.0 0 2015 5 6
1-11GN3TJ 新桑塔纳 118800.0 大众 安徽省 宿州海昌汽车销售服务有限责任公司 -1700.0 495.0 2 2017 7 8
1-11GNTKY 全新一代桑塔纳 119900.0 大众 北京市 北京市艾潇汽车有限公司 -143900.0 3.0 2 2016 3 3
1-11HMNQQ 全新途观L 355800.0 大众 贵州省 贵州华通众源汽车贸易服务有限公司 63900.0 493.0 6 2017 7 7
... ... ... ... ... ... ... ... ... ... ... ...
1-Z7U-601 凌渡 200000.0 大众 河北省 霸州市盛德汽车销售服务有限公司 24100.0 947.0 4 2016 7 8
1-ZG3W1L 全新帕萨特 262900.0 大众 北京市 北京车谷汽车销售有限公司 76000.0 716.0 4 2018 2 5
1-ZPOITB 全新一代桑塔纳 116900.0 大众 河北省 保定市聚合汽车维修有限公司 4000.0 39.0 2 2016 3 31
1-ZUL-1836 全新途观 239800.0 大众 四川省 四川广博汽车有限公司 95100.0 1403.0 4 2017 10 8
1-ZUZ-86 速派 211160.0 斯柯达 陕西省 陕西锐风汽车销售服务有限公司 26560.0 172.0 4 2014 5 26

3343 rows × 11 columns

In [90]:
df[col_to_use].groupby(df['车主ID']).mean()
Out[90]:
开票价格 价格差异 时间差异 车款评级
车主ID
1-10LD-2240 256600.0 -8400.0 6.0 4.0
1-10ZA-842 131900.0 -163800.0 510.0 2.0
1-11GN3TJ 119650.0 -1700.0 495.0 2.0
1-11GNTKY 191850.0 -143900.0 3.0 3.0
1-11HMNQQ 323850.0 63900.0 493.0 5.0
... ... ... ... ...
1-Z7U-601 187950.0 24100.0 947.0 3.0
1-ZG3W1L 224900.0 76000.0 716.0 3.0
1-ZPOITB 114900.0 4000.0 39.0 2.0
1-ZUL-1836 192250.0 95100.0 1403.0 3.0
1-ZUZ-86 197880.0 26560.0 172.0 3.0

3343 rows × 4 columns

In [91]:
train['开票价格'] = df[col_to_use].groupby(df['车主ID']).mean()['开票价格']
train['价格差异'] = df[col_to_use].groupby(df['车主ID']).mean()['价格差异']
train['时间差异'] = df[col_to_use].groupby(df['车主ID']).mean()['时间差异']
train['车款评级'] = df[col_to_use].groupby(df['车主ID']).mean()['车款评级']
In [92]:
train.head()
Out[92]:
市场车型 开票价格 品牌 所购买省份 所购买公司 价格差异 时间差异 车款评级 购买年份 购买月份 购买日
车主ID
1-10LD-2240 全新帕萨特 256600.0 大众 河北省 沧州兴华汽车销售服务有限公司 -8400.0 6.0 4.0 2013 12 16
1-10ZA-842 全新朗逸 131900.0 大众 江苏省 南京公用发展股份有限公司 -163800.0 510.0 2.0 2015 5 6
1-11GN3TJ 新桑塔纳 119650.0 大众 安徽省 宿州海昌汽车销售服务有限责任公司 -1700.0 495.0 2.0 2017 7 8
1-11GNTKY 全新一代桑塔纳 191850.0 大众 北京市 北京市艾潇汽车有限公司 -143900.0 3.0 3.0 2016 3 3
1-11HMNQQ 全新途观L 323850.0 大众 贵州省 贵州华通众源汽车贸易服务有限公司 63900.0 493.0 5.0 2017 7 7
In [93]:
df_key_customer_info.head()
Out[93]:
性别 教育程度 职位 行业 家庭年收入 家庭成员人数 是否拥有驾照 省份 城市 年龄
车主ID
1-8MN-68814 初中 总裁/总经理/总监/企业高管 家居、装饰 500000.0 4.0 Y 北京市 北京市 43.695031
1-3YPW-6314 本科 公司拥有者(老板)/合伙人 家居、装饰 500000.0 4.0 Y 江西省 景德镇市 40.000000
1-8LJ-21917 高中 总裁/总经理/总监/企业高管 电气、电器、仪器制造行业 100000.0 3.0 Y 河北省 衡水市 51.333333
1-8MK-70795 高中 总裁/总经理/总监/企业高管 电气、电器、仪器制造行业 230000.0 3.0 Y 安徽省 合肥市 57.000000
1-3S29-22819 高中 总裁/总经理/总监/企业高管 电气、电器、仪器制造行业 230000.0 3.0 Y 山东省 菏泽市 37.000000

下面把两个表格合并起来,信息交互。

In [94]:
df1 = train
df2 = df_key_customer_info

df_merge = df1.merge(df2,on = '车主ID',how = 'inner')
In [95]:
df_merge.shape
Out[95]:
(3120, 21)
In [96]:
df_merge.info()
<class 'pandas.core.frame.DataFrame'>
Index: 3120 entries, 1-10LD-2240 to 1-ZUZ-86
Data columns (total 21 columns):
 #   Column  Non-Null Count  Dtype  
---  ------  --------------  -----  
 0   市场车型    3120 non-null   object 
 1   开票价格    3120 non-null   float64
 2   品牌      3120 non-null   object 
 3   所购买省份   3120 non-null   object 
 4   所购买公司   3120 non-null   object 
 5   价格差异    3120 non-null   float64
 6   时间差异    3120 non-null   float64
 7   车款评级    3120 non-null   float64
 8   购买年份    3120 non-null   int64  
 9   购买月份    3120 non-null   int64  
 10  购买日     3120 non-null   int64  
 11  性别      3120 non-null   object 
 12  教育程度    3120 non-null   object 
 13  职位      3120 non-null   object 
 14  行业      3120 non-null   object 
 15  家庭年收入   3120 non-null   float64
 16  家庭成员人数  3120 non-null   float64
 17  是否拥有驾照  3120 non-null   object 
 18  省份      3120 non-null   object 
 19  城市      3120 non-null   object 
 20  年龄      3120 non-null   float64
dtypes: float64(7), int64(3), object(11)
memory usage: 536.2+ KB
In [97]:
df_merge['年龄'] = df_merge['年龄'].apply(lambda x: round(x,2))
In [98]:
df_merge.head()
Out[98]:
市场车型 开票价格 品牌 所购买省份 所购买公司 价格差异 时间差异 车款评级 购买年份 购买月份 购买日 性别 教育程度 职位 行业 家庭年收入 家庭成员人数 是否拥有驾照 省份 城市 年龄
车主ID
1-10LD-2240 全新帕萨特 256600.0 大众 河北省 沧州兴华汽车销售服务有限公司 -8400.0 6.0 4.0 2013 12 16 高中 公司拥有者(老板)/合伙人 汽车、摩托车及配件 100000.0 3.0 Y 河北省 沧州市 38.00
1-10ZA-842 全新朗逸 131900.0 大众 江苏省 南京公用发展股份有限公司 -163800.0 510.0 2.0 2015 5 6 大专 公司拥有者(老板)/合伙人 电气、电器、仪器制造行业 230000.0 3.0 Y 江苏省 南京市 55.00
1-11GN3TJ 新桑塔纳 119650.0 大众 安徽省 宿州海昌汽车销售服务有限责任公司 -1700.0 495.0 2.0 2017 7 8 高中 公司拥有者(老板)/合伙人 其它,请注明 500000.0 2.0 Y 安徽省 宿州市 60.33
1-11GNTKY 全新一代桑塔纳 191850.0 大众 北京市 北京市艾潇汽车有限公司 -143900.0 3.0 3.0 2016 3 3 本科 一般职员/技术人员 电气、电器、仪器制造行业 65000.0 3.0 Y 河北省 沧州市 41.50
1-11HMNQQ 全新途观L 323850.0 大众 贵州省 贵州华通众源汽车贸易服务有限公司 63900.0 493.0 5.0 2017 7 7 初中 公司拥有者(老板)/合伙人 建筑、路桥、建材 65000.0 4.0 Y 贵州省 贵阳市 55.67

这里,我们把车款评级作为target,先来看看类别的平衡度

In [99]:
data = df_merge
data['车款评级'] = round(data['车款评级'])

plt.figure(figsize = [12,8])
plt.title('车款评级直方图')
sns.countplot(data['车款评级'])
Out[99]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c74551d888>

可以看出车款评级结果的分布比较均匀。

分割成训练集和测试集

In [100]:
train,test = train_test_split(data,test_size = 0.33,random_state = 24)
In [101]:
print('训练数据集的维度是:',train.shape)
print('测试数据集的维度是:',test.shape)
训练数据集的维度是: (2090, 21)
测试数据集的维度是: (1030, 21)

特征工程

下面要对categorical变量进行encoding处理,对其进行适当的编码转换,从而为带入模型做准备。

最终我们处理好的训练数据中有21个客户特征,我们要对其进行筛选以及编码。

注意到:

  1. 不同车型有76种,较多
  2. 所采购公司有1000多个,较多,无法用传统编码方式处理
  3. 所在城市、省份等数据不好量化,也不好编码

所以我写了target encoding的相关代码,帮助处理这种情况,在避免维度爆炸的情况下,尽量保存客户的信息,并以比率的形式展现。

In [102]:
data.head()
Out[102]:
市场车型 开票价格 品牌 所购买省份 所购买公司 价格差异 时间差异 车款评级 购买年份 购买月份 购买日 性别 教育程度 职位 行业 家庭年收入 家庭成员人数 是否拥有驾照 省份 城市 年龄
车主ID
1-10LD-2240 全新帕萨特 256600.0 大众 河北省 沧州兴华汽车销售服务有限公司 -8400.0 6.0 4.0 2013 12 16 高中 公司拥有者(老板)/合伙人 汽车、摩托车及配件 100000.0 3.0 Y 河北省 沧州市 38.00
1-10ZA-842 全新朗逸 131900.0 大众 江苏省 南京公用发展股份有限公司 -163800.0 510.0 2.0 2015 5 6 大专 公司拥有者(老板)/合伙人 电气、电器、仪器制造行业 230000.0 3.0 Y 江苏省 南京市 55.00
1-11GN3TJ 新桑塔纳 119650.0 大众 安徽省 宿州海昌汽车销售服务有限责任公司 -1700.0 495.0 2.0 2017 7 8 高中 公司拥有者(老板)/合伙人 其它,请注明 500000.0 2.0 Y 安徽省 宿州市 60.33
1-11GNTKY 全新一代桑塔纳 191850.0 大众 北京市 北京市艾潇汽车有限公司 -143900.0 3.0 3.0 2016 3 3 本科 一般职员/技术人员 电气、电器、仪器制造行业 65000.0 3.0 Y 河北省 沧州市 41.50
1-11HMNQQ 全新途观L 323850.0 大众 贵州省 贵州华通众源汽车贸易服务有限公司 63900.0 493.0 5.0 2017 7 7 初中 公司拥有者(老板)/合伙人 建筑、路桥、建材 65000.0 4.0 Y 贵州省 贵阳市 55.67
In [103]:
# target encoding
def add_noise(series, noise_level):
    return series * (1 + noise_level * np.random.randn(len(series)))

def target_encode(trn_series=None, 
                  tst_series=None, 
                  target=None, 
                  min_samples_leaf=1, 
                  smoothing=1,
                  noise_level=0):
    assert len(trn_series) == len(target)
    assert trn_series.name == tst_series.name
    temp = pd.concat([trn_series, target], axis=1)
    # Compute target mean 
    averages = temp.groupby(by=trn_series.name)[target.name].agg(["mean", "count"])
    # Compute smoothing
    smoothing = 1 / (1 + np.exp(-(averages["count"] - min_samples_leaf) / smoothing))
    # Apply average function to all target data
    prior = target.mean()
    # The bigger the count the less full_avg is taken into account
    averages[target.name] = prior * (1 - smoothing) + averages["mean"] * smoothing
    averages.drop(["mean", "count"], axis=1, inplace=True)
    # Apply averages to trn and tst series
    ft_trn_series = pd.merge(
        trn_series.to_frame(trn_series.name),
        averages.reset_index().rename(columns={'index': target.name, target.name: 'average'}),
        on=trn_series.name,
        how='left')['average'].rename(trn_series.name + '_mean').fillna(prior)
    # pd.merge does not keep the index so restore it
    ft_trn_series.index = trn_series.index 
    ft_tst_series = pd.merge(
        tst_series.to_frame(tst_series.name),
        averages.reset_index().rename(columns={'index': target.name, target.name: 'average'}),
        on=tst_series.name,
        how='left')['average'].rename(trn_series.name + '_mean').fillna(prior)
    # pd.merge does not keep the index so restore it
    ft_tst_series.index = tst_series.index
    return add_noise(ft_trn_series, noise_level), add_noise(ft_tst_series, noise_level)
In [104]:
data.columns
Out[104]:
Index(['市场车型', '开票价格', '品牌', '所购买省份', '所购买公司', '价格差异', '时间差异', '车款评级', '购买年份',
       '购买月份', '购买日', '性别', '教育程度', '职位', '行业', '家庭年收入', '家庭成员人数', '是否拥有驾照',
       '省份', '城市', '年龄'],
      dtype='object')
In [105]:
col_to_encode = ['市场车型','所购买省份','所购买公司','行业','省份','城市']
In [106]:
for col in col_to_encode:
    train_encoded, test_encoded = target_encode(train[col], 
                             test[col], 
                             target=train['车款评级'], 
                             min_samples_leaf=100,
                             smoothing=10,
                             noise_level=0.01)
    train[col] = train_encoded
    test[col] = test_encoded
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:8: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:9: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  if __name__ == '__main__':

接下来对其它有阶级区分度的特征进行标签编码(label encoding),他们之间是有顺序的,例如:大学本科 $>$ 大专,所以前者编码为1,后者为0

In [107]:
train['品牌'] = train['品牌'].replace({'大众':1,'斯柯达':0})
test['品牌'] = test['品牌'].replace({'大众':1,'斯柯达':0})
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
In [108]:
train['职位'] = train['职位'].replace({'不想说/不愿回答':0,
                                       '其它':1,
                                       '自由职业者':2,
                                  '事业单位/政府职员或官员':3,
                                  '一般职员/技术人员':4,
                                  '专业人士':5,
                                  '部门经理/主管/高级技术人员':6,
                                  '总裁/总经理/总监/企业高管':7,
                                  '公司拥有者(老板)/合伙人':8})

test['职位'] = test['职位'].replace({'不想说/不愿回答':0,
                                       '其它':1,
                                       '自由职业者':2,
                                  '事业单位/政府职员或官员':3,
                                  '一般职员/技术人员':4,
                                  '专业人士':5,
                                  '部门经理/主管/高级技术人员':6,
                                  '总裁/总经理/总监/企业高管':7,
                                  '公司拥有者(老板)/合伙人':8})
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:9: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  if __name__ == '__main__':
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:19: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
In [109]:
train['教育程度'] = train['教育程度'].replace({'不想说/不愿回答':0,
                                      '其它':1,
                                      '初中':2,
                                      '高中':3,
                                      '大专':4,
                                      '本科':5,
                                      '硕士':6,
                                      '博士及以上':7})

test['教育程度'] = test['教育程度'].replace({'不想说/不愿回答':0,
                                      '其它':1,
                                      '初中':2,
                                      '高中':3,
                                      '大专':4,
                                      '本科':5,
                                      '硕士':6,
                                      '博士及以上':7})
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:8: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:17: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
In [110]:
train['性别'] = train['性别'].replace({'女':0,'男':1})
test['性别'] = test['性别'].replace({'女':0,'男':1})
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
In [111]:
train.columns
Out[111]:
Index(['市场车型', '开票价格', '品牌', '所购买省份', '所购买公司', '价格差异', '时间差异', '车款评级', '购买年份',
       '购买月份', '购买日', '性别', '教育程度', '职位', '行业', '家庭年收入', '家庭成员人数', '是否拥有驾照',
       '省份', '城市', '年龄'],
      dtype='object')
In [112]:
train['是否拥有驾照'] = train['是否拥有驾照'].replace({'N':0,'Y':1})
test['是否拥有驾照'] = test['是否拥有驾照'].replace({'N':0,'Y':1})
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
In [113]:
train['性别'] = train['性别'].replace('没有回答',np.nan)
test['性别'] = test['性别'].replace('没有回答',np.nan)
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
In [114]:
train['性别'] = train['性别'].fillna(method = 'ffill')
test['性别'] = test['性别'].fillna(method = 'ffill')
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:1: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  """Entry point for launching an IPython kernel.
C:\Users\gzjgz\anaconda3\lib\site-packages\ipykernel_launcher.py:2: SettingWithCopyWarning: 
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  
In [115]:
train.head()
Out[115]:
市场车型 开票价格 品牌 所购买省份 所购买公司 价格差异 时间差异 车款评级 购买年份 购买月份 购买日 性别 教育程度 职位 行业 家庭年收入 家庭成员人数 是否拥有驾照 省份 城市 年龄
车主ID
1-1CSHO0V 3.483511 239100.0 1 2.741457 2.627655 64300.0 675.5 4.0 2020 5 27 1.0 5 8 2.463632 65000.0 3.0 1 2.677041 2.638550 35.56
1-8LP-90426 2.648125 168900.0 1 2.726264 2.627958 -8000.0 479.0 2.0 2014 4 28 1.0 4 0 2.868560 100000.0 3.0 1 2.589981 2.553302 46.50
1-46ODUMZ 2.647397 168450.0 1 2.641716 2.630995 55100.0 753.0 2.0 2020 1 19 0.0 4 3 2.879871 65000.0 3.0 1 2.620548 2.629266 34.00
1-1S5-98135 2.627264 212600.0 0 2.526898 2.662170 64600.0 61.0 3.0 2012 2 25 1.0 5 4 2.630693 65000.0 3.0 1 2.572142 2.602559 51.50
1-8FS-15848 2.634467 50000.0 1 2.654928 2.654739 0.0 222.0 0.0 2007 2 14 1.0 6 0 2.472791 100000.0 3.0 1 2.619158 2.718782 29.50
In [116]:
test.head()
Out[116]:
市场车型 开票价格 品牌 所购买省份 所购买公司 价格差异 时间差异 车款评级 购买年份 购买月份 购买日 性别 教育程度 职位 行业 家庭年收入 家庭成员人数 是否拥有驾照 省份 城市 年龄
车主ID
1-18T5X0R 2.636144 157250.0 0 2.609284 2.630323 10700.0 77.0 2.0 2016 11 14 1.0 5 3 2.686165 65000.0 3.0 1 2.648530 2.644588 33.00
1-XOL58R 3.188376 266250.0 1 2.693992 2.647845 9300.0 200.0 4.0 2016 8 19 1.0 5 0 2.477465 65000.0 3.0 1 2.636150 2.582867 33.00
1-8GY-51680 3.230869 153150.0 1 2.575592 2.674970 206300.0 2799.0 2.0 2017 11 21 0.0 4 0 2.445586 65000.0 3.0 1 2.674882 2.626331 49.00
1-3FZ0-4739 2.692676 128450.0 0 2.629005 2.658653 12900.0 102.0 2.0 2015 5 15 1.0 5 4 2.647529 100000.0 3.0 1 2.639664 2.636107 35.33
1-8I2-1136 2.619543 182150.0 1 2.628727 2.649557 -17300.0 1966.0 2.0 2016 4 6 1.0 5 0 2.464505 65000.0 3.0 1 2.648655 2.595000 39.00

EDA

In [117]:
train.columns
Out[117]:
Index(['市场车型', '开票价格', '品牌', '所购买省份', '所购买公司', '价格差异', '时间差异', '车款评级', '购买年份',
       '购买月份', '购买日', '性别', '教育程度', '职位', '行业', '家庭年收入', '家庭成员人数', '是否拥有驾照',
       '省份', '城市', '年龄'],
      dtype='object')
In [118]:
plt.figure(figsize=(20,7))
col = ['市场车型', '开票价格', '品牌', '所购买省份', '所购买公司', '价格差异', '时间差异', '车款评级', '购买年份',
       '购买月份', '购买日', '性别', '教育程度', '职位', '行业', '家庭年收入', '家庭成员人数', '是否拥有驾照',
       '省份', '城市', '年龄']
sns.heatmap(train[col].corr(), cmap = 'Blues',linewidths = 0.1,annot = True)
Out[118]:
<matplotlib.axes._subplots.AxesSubplot at 0x1c7445f3a08>

通过热图可以发现特征之间的相关性,很明显,车款评级和市场车型、开票价格、购买年份等因素较为相关。

In [119]:
plt.figure(figsize = [12,8])
sns.lmplot(x = "购买年份", y = "开票价格", data = train, hue = '车款评级', palette="Set1", scatter_kws = {"alpha":0.3})
Out[119]:
<seaborn.axisgrid.FacetGrid at 0x1c745afe848>
<Figure size 864x576 with 0 Axes>

可看出等级越高的车,随着购买年份的推移,开票价格也越贵。

In [120]:
plt.figure(figsize = [12,8])
sns.lmplot(x = "职位", y = "行业", data = train, hue = '车款评级', palette="Set1", scatter_kws = {"alpha":0.3})
Out[120]:
<seaborn.axisgrid.FacetGrid at 0x1c7449e35c8>
<Figure size 864x576 with 0 Axes>

可看出评级最低和最高的两款车型下,客户的职位和行业相关度最高。

下面对数据进行归一化

In [121]:
train.columns
Out[121]:
Index(['市场车型', '开票价格', '品牌', '所购买省份', '所购买公司', '价格差异', '时间差异', '车款评级', '购买年份',
       '购买月份', '购买日', '性别', '教育程度', '职位', '行业', '家庭年收入', '家庭成员人数', '是否拥有驾照',
       '省份', '城市', '年龄'],
      dtype='object')
In [122]:
scaler = StandardScaler()
feature_col = ['市场车型', '开票价格', '品牌', '所购买省份', '所购买公司', '价格差异', '时间差异', '购买年份',
       '购买月份', '购买日', '性别', '教育程度', '职位', '行业', '家庭年收入', '家庭成员人数', '是否拥有驾照',
       '省份', '城市', '年龄']
train_scale = pd.DataFrame(scaler.fit_transform(train.drop(['车款评级'], axis=1)), columns = feature_col)
test_scale = pd.DataFrame(scaler.transform(test.drop(['车款评级'], axis=1)), columns = feature_col)

XGBoost 建模

In [123]:
train_scale
Out[123]:
市场车型 开票价格 品牌 所购买省份 所购买公司 价格差异 时间差异 购买年份 购买月份 购买日 性别 教育程度 职位 行业 家庭年收入 家庭成员人数 是否拥有驾照 省份 城市 年龄
0 2.043542 0.831396 0.415740 1.096678 -0.787320 0.044232 -0.312187 1.328583 -0.469834 1.172891 0.590980 0.846682 1.288220 -1.175410 -0.504642 -0.140017 0.307919 0.610972 0.191215 -0.998162
1 -0.609834 -0.218510 0.415740 0.978727 -0.775785 -0.568403 -0.372507 -0.388279 -0.764891 1.286200 0.590980 -0.091582 -1.460859 1.086223 -0.136197 -0.140017 0.307919 -0.079313 -0.912642 0.358739
2 -0.612147 -0.225241 0.415740 0.322359 -0.659927 -0.033725 -0.288397 1.328583 -1.650065 0.266413 -1.692104 -0.091582 -0.429955 1.149396 -0.504642 -0.140017 0.307919 0.163043 0.071000 -1.191650
3 -0.676095 0.435064 -2.405351 -0.568997 0.529119 0.046774 -0.500822 -0.960566 -1.355007 0.946271 0.590980 0.846682 -0.086320 -0.242331 -0.504642 -0.140017 0.307919 -0.220760 -0.274830 0.978895
4 -0.653216 -1.996771 0.415740 0.424934 0.245673 -0.500615 -0.451399 -2.391285 -1.355007 -0.300135 0.590980 1.784945 -1.460859 -1.124252 -0.136197 -0.140017 0.307919 0.152024 1.230124 -1.749791
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2085 -0.576707 0.020784 0.415740 -2.006024 -1.006361 -0.688727 -0.180343 -0.102135 -0.764891 1.399510 -1.692104 0.846682 -0.086320 -0.125803 1.232317 -0.140017 0.307919 -1.660511 0.024589 -0.943588
2086 -1.095773 0.603318 0.415740 -0.771226 -0.338531 -1.957210 -0.203366 0.470152 -1.355007 0.493033 0.590980 -0.091582 1.288220 -1.141484 -0.504642 -0.140017 0.307919 -0.224608 -0.217022 -1.191650
2087 -0.612088 -1.102406 0.415740 0.306136 0.020289 -0.500615 -0.439427 -0.960566 -1.059949 -1.093303 0.590980 0.846682 0.944585 -0.324939 -0.136197 -0.140017 0.307919 0.691457 -0.188382 -0.447463
2088 1.157217 -0.083907 0.415740 0.424559 -0.455586 0.378935 -0.311727 0.470152 -1.059949 0.266413 0.590980 -0.091582 -0.086320 -0.121377 -0.136197 -0.140017 0.307919 0.389143 -0.282766 1.164942
2089 -0.640183 -0.807774 0.415740 -0.393756 -1.068231 0.846672 -0.395837 -1.532854 -1.650065 -1.773161 0.590980 -0.091582 0.944585 1.401657 -0.504642 -0.140017 0.307919 -0.223144 -0.016178 -0.075370

2090 rows × 20 columns

In [124]:
train['车款评级'].values
Out[124]:
array([4., 2., 2., ..., 2., 3., 2.])
In [125]:
train_scale['车款评级'] = train['车款评级'].values
In [126]:
X = train_scale.drop('车款评级',axis=1)
y = train["车款评级"]
In [127]:
def modelfit(alg, dtrain, predictors, useTrainCV=True, cv_folds=5, early_stopping_rounds=50, target = '车款评级'):
    
    '''
    alg: 算法
    dtrain: 训练集包含target
    predictors: 特征column
    '''
    if useTrainCV:
        xgb_param = alg.get_xgb_params()
        xgtrain = xgb.DMatrix(dtrain[predictors].values, label=dtrain[target].values)
        cvresult = xgb.cv(xgb_param, xgtrain, num_boost_round=alg.get_params()['n_estimators'], nfold=cv_folds,
            metrics='mlogloss', early_stopping_rounds=early_stopping_rounds)
        alg.set_params(n_estimators=cvresult.shape[0])
    
    #Fit the algorithm on the data
    alg.fit(dtrain[predictors], dtrain[target],eval_metric='mlogloss')
        
    #Predict training set:
    dtrain_predictions = alg.predict(dtrain[predictors])
    dtrain_predprob = alg.predict_proba(dtrain[predictors])[:,1]
        
    #Print model report:
    print ("\nModel Report")
    print ("Accuracy : %.4g" % metrics.accuracy_score(dtrain[target].values, dtrain_predictions))
    print(confusion_matrix(dtrain_predictions,dtrain[target].values))
    print(classification_report(dtrain_predictions,dtrain[target].values))

    pd.DataFrame(alg.predict_proba(dtrain[predictors])).plot()
    plt.show()

    sns.heatmap(pd.DataFrame(confusion_matrix(dtrain_predictions,dtrain[target].values)), annot=True, cmap='inferno', fmt="d")
    plt.show()


    feat_imp = pd.Series(alg.get_booster().get_fscore()).sort_values(ascending=False)
    feat_imp.plot(kind='bar', title='Feature Importances')
    plt.ylabel('Feature Importance Score')
In [128]:
predictors = [x for x in train_scale.columns[:-1]]

xgb1 = XGBClassifier(
 learning_rate =0.1,
 n_estimators=60,
 max_depth=4,
 min_child_weight=1,
 gamma=0,
 subsample=0.8,
 colsample_bytree=0.8,
 objective= 'multi:softmax',
 num_class = 7,
 nthread=4,
 seed=2)

modelfit(xgb1, train_scale, predictors, target = '车款评级')
Model Report
Accuracy : 0.9962
[[113   0   0   0   0   0   0]
 [  0 137   0   0   0   0   0]
 [  0   0 727   2   0   0   0]
 [  0   0   2 660   2   0   0]
 [  0   0   0   2 323   0   0]
 [  0   0   0   0   0  83   0]
 [  0   0   0   0   0   0  39]]
              precision    recall  f1-score   support

         0.0       1.00      1.00      1.00       113
         1.0       1.00      1.00      1.00       137
         2.0       1.00      1.00      1.00       729
         3.0       0.99      0.99      0.99       664
         4.0       0.99      0.99      0.99       325
         5.0       1.00      1.00      1.00        83
         6.0       1.00      1.00      1.00        39

    accuracy                           1.00      2090
   macro avg       1.00      1.00      1.00      2090
weighted avg       1.00      1.00      1.00      2090

根据XGBoost的分类结果,准确度达到了99%,意味着我们能较为准确地通过对客户的数据特征,对其可能会回购的车款级别做出预测,并且准确率达到99%。

例如:我们知道了客户的第一次购车价格,购车的时间差,所处行业等等信息,可以来推测他们可能会购买的车型,如果分到了第2等级的车款,我们会从此等级的车里为其推荐,并根据每款车的销量加权,首推排名前5的车款进行推荐。

客户服务分析

In [129]:
df_service_request.shape
Out[129]:
(1447, 57)

客户服务数据仅有1447条,之前的两个数据都是3300条以上,所以很难将其合并,会丢失掉很多数据,也说明50%的客户没有申请服务

In [130]:
df_service_request.columns
Out[130]:
Index(['车主ID', '服务品牌', '服务工单编号', '关联工单号', '工单状态', '性质分类', '业务分类', '工单产生类型',
       '工单一级分类', '工单二级分类', '工单三级分类', '工单来源', '工单创建日期', '年', '月', '工单来电描述',
       '工单处理层面', '处理部门', '省份', '车型大类', '车型', '车型6位码', '车色', '购车时间', '车辆VIN码',
       '行驶里程', '涉及机构类型', '涉及机构代码', '涉及机构名称', '所属分销中心', '当前处理人', '当前处理机构',
       '当前处理经销商', '上次处理人工号', '当前处理人工号', '故障产品分类', '工单实际响应时间', '工单实际完成时间',
       '是否及时响应', '是否及时完成', '是否一次解决', '是否记录投诉次数', '是否重大事故', '操作历史当前操作部门',
       '操作历史当前操作人', '操作历史当前操作时间', '操作历史操作类型', '系统登陆用户名', '姓名', '是否媒体工单',
       '媒体类别一级', '媒体类别二级', '媒体名称', '是否与召回有关', '是否普遍问题', '最后一次操作内容', '用户咨询内容'],
      dtype='object')
In [131]:
mypie(df_service_request,'服务品牌')
# 服务品牌为大众和斯柯达
In [132]:
mypie(df_service_request,'工单状态')
# 处理完成比例高达98%,说明服务效果很好,客户满意度不错。
In [133]:
mypie(df_service_request,'性质分类') 
# 咨询业务占比达到84%,投诉比例仅占15.1%,说明客户反馈相对不错
In [134]:
mypie(df_service_request,'业务分类') 
# 业务主要集中在流转、分配以及售后,所以有必要提升工单的流转分配效率,84%的业务都来自于此。
In [135]:
mypie(df_service_request,'工单一级分类') 
# 工单一级分类主要包括大众俱乐部相关业务、维修保养以及产品技术。技术问题的服务仅占10%,而关于大众俱乐部相关事宜占比接近30%,可能需要更多相关业务支持。
In [136]:
mypie(df_service_request,'工单二级分类') 
# 工单二级分类主要包括保修问题、大众售后以及配件问题,由于斯柯达的业务比重相对较小,工单二级分类也仅占3.3%,位列最后。
In [137]:
mypie(df_service_request,'工单三级分类') 
# 关于工单三级分类,30%左右的问题是关于订货和配件问题,其他问题基本均匀分布,都是关于不同型号的汽车问题。

我认为通过对客户反馈的统计分析,能更好的了解客户偏好和业务侧重点,能够更好的流转和分配。

Summary

客户(车主)回购数据囊括了其市场车型、购买日期、开票价格、所购地址等等。我通过数据分析、数据挖掘以及机器学习建模,对客户的重购可能购买的车款进行评级和预测,最终,我得出的结论如下: ​

  • 6747位客户有重购行为,486位顾客暂未重购,且销量最好的车是全新帕萨特(11.33%)。
  • 重购车辆的价格会比首购平均高出46000元左右,变化量的中位数在7000元左右,最大价格差在220左右,最小则为-48万左右,差异变动很大,但整体成正态分布。
  • 当天重购和当月重购的比率接近(4%),而大部分客户选择在第一年内重购(16%),在第二、三、四、五年里重购的比重也十分相似(11%),之后重购率稳步下降。
  • 一共有76种不同车型,平均价格在18万3千元左右,我根据对价格的分析结果对其进行评级(共7个等级),从而为后面机器学习的分类任务埋下伏笔。
  • 通过连接客户基本信息数据和客户回购信息数据进行信息交互,整合出了3120条有效数据以及21个有效特征。
  • 对新数据进行数据分析、特征工程、数据转换以及利用XGBoost算法对数据根据车款评级分类进行建模,最终模型准确率达到99%,并可以通过评级内车款的销量加权得到相应的回购概率,筛选前五中车款进行推荐。
  • 客服数据一共1447条,57列,其中很多特征并不适合对回购情况进行分析,而且合并会丢失将近50%的客户信息。
  • 根据工单一级到三级的业务占比,主要都是集中在大众品牌上的俱乐部事宜、维修、保修、配件以及订单问题,而且工单流转分配效率较高。