项目背景
随着智能手机的普及和手机用户的激增,共享单车作为城市交通系统的一个重要组成部分,以绿色环保、便捷高效、经济环保为特征蓬勃发展。共享单车企业通过在校园、公交站点、居民区、公共服务区等提供服务,完成交通行业最后一块“拼图”,与其他公共交通方式产生协同效应。共享单车有助于缓解城市短距离交通出行和“最后一公里”难题,但共享单车由于其运营特点,对企业在城市投放和调度单车的规划管理方面,存在较大挑战。
基于上述背景,本文基于上海摩拜单车的2016年8月份随机抽样大约10万条的开放订单数据进行分析,挖掘出数据背后的规律,用数据勾勒出摩拜共享单车的使用与用户出行现状,从而有助于摩拜共享单车企业更好地推出营销策略,定位新单车的投放区域,调控车辆布置,更好地服务用户。
注:本项目的数据、代码和图表可戳:摩拜共享单车数据分析项目数据、代码、图表 下载
数据探索
这一步中我们统观数据的全貌,对数据有个大体的了解,对数据进行质量探索和特征分析。
读取数据并查看数据集数据
import pandas as pd
from math import radians, cos, sin, asin, sqrt,ceil
import numpy as np
import geohash
#数据读取
data = pd.read_csv("./mobike_shanghai_sample_updated.csv")
print(data.head(10))
该数据集为摩拜共享单车企业提供的上海城区2016年8月随机抽样的10万多条用户骑行用车数据(订单数据),包含交易编号、用户ID、车辆ID、骑行起点经纬度、骑行终点经纬度、租赁时间、还车时间、骑行轨迹经纬度等数据。
查看数据集属性类型,在这里我们可以看出租赁时间和还车时间的数据类型为object类型,我们紧接着可以把它转化为datetime类型
print(data.info())
data['start_time'] = pd.to_datetime(data['start_time'])
data['end_time'] = pd.to_datetime(data['end_time'])
print(data.info())
查看数据集的空值分布情况,数据不存在空值
print(data.isnull().sum())
数据挖掘
该数据集虽然多达10万多条用车交易数据,但每一条用车交易数据里只有10个特征,并且该数据集和之前的电影数据集不同,之前的电影数据集每一个特征都是独立的信息字段,不和其他特征产生明显关联,而该数据集的特征之间拥有明显的相关性,我们可以通过关联组合特征之间的关系得到新的特征,或者从一个特征反映出来的多个方面组合出多个新的特征扩充数据集,掌握事物的多个方面,挖掘出数据更多潜在的规律,从而使得后面的数据分析可以进行更多维度的分析,得到对数据的更多认识。
如何发现新特征:
- 租赁时间 + 还车时间 => 骑行时长
- 骑行起点经纬度 + 骑行终点经纬度 => 骑行的位移
- 摩拜单车骑行轨迹经纬度 => 骑行的路径
- 租赁时间 => 星期几(即每笔骑行订单发生在星期几) + 时间段(即每笔订单发生在一天24小时的哪个时间段)
- 骑行时长 => 订单金额(粗略估计)
- 每笔订单金额 + 每笔订单租赁时间 + 每笔订单用户ID=> 用户分级(RFM模型)
- 骑行起点终点经纬度 => 骑行起点终点所处的地区
租赁时间 + 还车时间 => 骑行时长
新增“lag”列,通过开始时间 - 结束时间计算得到骑行时长,并把时长单位统一为分钟。
data["lag"] = (data.end_time - data.start_time).dt.seconds/60
骑行起点经纬度 + 骑行终点经纬度 => 骑行的位移
新增“distance”列,通过计算骑行起点和终点的经纬度得到骑行的位移,单位为千米。
geodistance()封装的是通过两点经纬度求两点直线距离的数学公式。对背后公式的推导过程和数学原理感兴趣的可以戳 https://blog.csdn.net/sunjianqiang12345/article/details/60393437 了解。
def geodistance(item):
lng1_r, lat1_r, lng2_r, lat2_r = map(radians, [item["start_location_x"], item["start_location_y"], item["end_location_x"], item["end_location_y,"]]) # 经纬度转换成弧度
dlon = lng1_r - lng2_r
dlat = lat1_r - lat2_r
dis = sin(dlat/2)**2 + cos(lat1_r) * cos(lat2_r) * sin(dlon/2)**2
distance = 2 * asin(sqrt(dis)) * 6371 * 1000 # 地球平均半径为6371km
distance = round(distance/1000,3)
return distance
#data按行应用geodistance()得到distance列的数值
data["distance"] = data.apply(geodistance,axis=1)
摩拜单车骑行轨迹经纬度 => 骑行的路径
新增“adderLength”列,通过计算“track”列数据得到骑行路径。
把骑行轨迹字符串按“#”分隔符分隔成列表,得到骑行轨迹的经纬度信息列表
通过不断两两轮询该骑行轨迹列表,每一次按顺序取出两个列表元素(前轨迹采样点的经纬度和后轨迹采样点的经纬度)按“,”分隔符分隔,得到四个值(前轨迹采样点的经度、前轨迹采样点的纬度、后轨迹采样点的经度、后轨迹采样点的纬度),组合成一个item字典传入给geodistance()计算出两点的位移,再把每一小段每一小段的位移累加起来得到骑行的路径。
#通过摩拜单车的踪迹获取每次交易骑行的路径
def geoaadderLength(item):
track_list = item["track"].split("#")
adderLength_item = {}
adderLength = 0
for i in range(len(track_list)-1):
start_loc = track_list[i].split(",")
end_loc = track_list[i+1].split(",")
adderLength_item["start_location_x"],adderLength_item["start_location_y"] = float(start_loc[0]),float(start_loc[1])
adderLength_item["end_location_x"],adderLength_item["end_location_y"] = float(end_loc[0]),float(end_loc[1])
adderLength_each = geodistance(adderLength_item)
adderLength = adderLength_each + adderLength
return adderLength
data["adderLength"] = data.apply(geoaadderLength,axis=1)
租赁时间 => 星期几 + 时间段(24小时制)
新增“weekday”(即每笔骑行订单发生在星期几)和“hour”(即每笔订单发生在一天24小时的哪个时间段)
data['weekday'] = data.start_time.apply(lambda x: x.isoweekday())
data['hour'] = data.start_time.apply(lambda x: x.utctimetuple().tm_hour)
骑行时长 => 订单金额
新增“cost”列,根据每笔订单的骑行时长,粗略估计订单金额,参照2016年摩拜收费标准,按每30分钟收取1元。
data['cost'] = data.lag.apply(lambda x: ceil(x/30))
订单金额 => 用户分级(RFM模型)
由于我们拥有了每笔交易的用户id、消费金额、消费时间,我们可以考虑运用RFM模型,对用户进行分级,这里采取的模型是RFM模型。
RFM模型是进行用户价值细分的一种方法,是用以研究用户的数学模型。
- R(Recency)最近一次消费时间:表示用户最近一次消费距离现在的时间;
- F(Frequency)消费频率:消费频率是指用户在统计周期内购买商品的次数;
- M(Monetary)消费金额:消费金额是指用户在统计周期内消费的总金额,体现了消费者为企业创利的多少;
这3个维度,帮助我们把用户划分为标准的8类
其模型构建过程可分为如下步骤:
- 计算RFM值
R值:即每个用户最后一次租赁共享单车时间距9月1日多少天;(因为数据集只包含2016年8月份的数据,所以我们站在9月1的时间节点上统计每个用户最近一次消费距离现在的时间)
F值:即每个用户累计租赁单车频次;
M值:即每个 用户累积消费金额;
#因数据集仅包含八月份发起的订单数据,故以9月1日为R值计算基准
data['r_value_single'] = data.start_time.apply(lambda x: 32 - x.timetuple().tm_mday)
# 按每个用户id所有订单日期距9/1相差天数的最小值作为r值
r_value = data.groupby(['userid']).r_value_single.min()
f_value = data.groupby(['userid']).size() # 按每个用户id八月累积订单数量作为f值
m_value = data.groupby(['userid']).cost.sum() # 按每个用户id八月累积消费金额作为m值
#把r值、f值、m值组合成DataFrame
rfm_df = pd.DataFrame({'r_value':r_value,'f_value':f_value,"m_value":m_value})
- 计算RFM分数
在这一步中我们给每个用户的RFM值打分,分值的大小取决于我们的偏好,RFM值越高,其RFM得分也越高。RFM模型中打分一般采取5分制,这里采取根据RFM值的区间划分,进行RFM分值的设置。
rfm_df["r_score"] = pd.cut(rfm_df["r_value"],5,labels=[5,4,3,2,1]).astype(float)
rfm_df["f_score"] = pd.cut(rfm_df["f_value"],5,labels=[1,2,3,4,5]).astype(float)
rfm_df["m_score"] = pd.cut(rfm_df["m_value"],5,labels=[1,2,3,4,5]).astype(float)
- 划分用户维度
现在r_score、f_score、m_score在1-5几个数之间,如果把这3个值进行组合,像111,112,113…这样可以组合出125(5 * 5 * 5)种结果,但过多的分类和不分类本质上是差不多的,所以在划分用户维度这一块,我们简化分组结果。我们通过判断每个客户的R、F、M分数是否大于其平均值,来简化分组结果。每个客户的RFM值和RFM平均值对比后只有0和1(0表示小于平均值,1表示大于平均值)两种结果,整体组合下来共有8个分组(2 * 2 * 2)。
#后面*1是为了把布尔值false和true转成0和1
rfm_df["r是否大于均值"] = (rfm_df["r_score"] > rfm_df["r_score"].mean())*1
rfm_df["f是否大于均值"] = (rfm_df["f_score"] > rfm_df["f_score"].mean())*1
rfm_df["m是否大于均值"] = (rfm_df["m_score"] > rfm_df["m_score"].mean())*1
#把每个用户的rfm三个指标统合起来
rfm_df["class_index"] = (rfm_df["R是否大于均值"]*100) + (rfm_df["f是否大于均值"]*10) + (rfm_df["m是否大于均值"]*1)
def transform_user_class(x):
if x == 111:
label = "重要价值用户"
elif x == 110:
label = "消费潜力用户"
elif x == 101:
label = "频次深耕用户"
elif x == 100:
label = "新用户"
elif x == 11:
label = "重要价值流失预警用户"
elif x == 10:
label = "一般用户"
elif x == 1:
label = "高消费唤回用户"
elif x == 0:
label = "流失用户"
return label
rfm_df["user_class"] = rfm_df["class_index"].apply(transform_user_class)
至此,RFM模型会把用户划分为标准的8类,其8类所对应的含义如下
把得到的用户分类结果按用户ID拼接到原有数据中,命名为“user_class”列
data = data.merge(rfm_df["user_class"], on = 'userid', how = 'inner')
骑行起点终点经纬度 => 骑行起点终点所处的区块
在这一步中我们把骑行起点终点的经纬度转换为GeoHash编码字符串。
GeoHash将二维的经纬度转换成GeoHash编码字符串,每一个字符串代表了某一矩形区域。这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串。GeoHash本质上是一个哈希函数,但和其他的一些哈希函数会尽力避免哈希碰撞不一样,GeoHash根据经纬度范围把不同的经纬度哈希碰撞到同一字符串。
Geohash能够提供任意经纬度的分段级别,一般分为1-12级。Geohash编码字符串越长,表示的区域范围越精确,GeoHash编码字符串长度所对应的区块范围。
针对摩拜共享单车的短途骑行特点,我们选择GeoHash6位编码进行转换。
在转换之前需要安装geohash
pip install geohash
并去python/Lib/site-packages/的目录下,把Geohash文件夹重命名为geohash,然后修改该目录下的init.py文件,把from geohash改为from .geohash,然后才能正常导入(这是个bug)。
import geohash
#经纬度转geohash
def transform_start_geohash(item):
return geohash.encode(item["start_location_x"], item["start_location_y"],6)
def transform_end_geohash(item):
return geohash.encode(item["end_location_x"], item["end_location_y"],6)
data["geohash_start_block"] = data.apply(transform_start_geohash,axis = 1)
data["geohash_end_block"] = data.apply(transform_end_geohash,axis = 1)
查看一下转换后的GeoHash编码字符串
print(data.loc[:,["geohash_start_block","geohash_end_block"]])
查看扩充后的数据集
查看扩充后的数据集并保存
print(data.head(10))
data.to_csv("./data_dig.csv",index = None)
扩充后的数据集增加了如下信息字段:
lag(骑行时长)、distance(骑行位移)、adderLength(骑行路径)、weekday(骑行的星期日期)、hour(骑行时间段)、cost(本次骑行的消费金额)、r_value_single(订单发起时间距离9月1日的天数)、user_class(用户分级)、geohash_start_block(骑行起点所在区块的GeoHash编码字符串)、geohash_end_block(骑行终点所在区块的GeoHash编码字符串)。
数据分析
针对经数据挖掘扩充后的数据集,我们从以下维度分析:
时间维度
- 骑行时长分布分析
from pyecharts import options as opts
from pyecharts.charts import Bar
import pandas as pd
#读取数据
data = pd.read_csv("./data_dig.csv")
#骑行时长分布
lag_data = data["lag"].value_counts()
lag_c = (
Bar()
.add_xaxis(lag_data.index.tolist())
.add_yaxis("骑行时长",lag_data.values.tolist())
.set_global_opts(title_opts=opts.TitleOpts(title="骑行时长分布图"))
.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
)
lag_c.render_notebook()
由图表我们观察到数据存在明显的长尾,呈现右偏态分布(正态分布和偏态分布都是正常现象,为什么数据呈现偏态分布?因为人们使用共享单车出行这一行为本身就含有“偏心”,倾向于短期出行);x轴的数据范围非常大,造成这样的结果有一部分是由于数据存在异常记录,由图表x轴的数据范围跨度,我们观察到有存在骑行时长长达955分的骑行记录,而连续骑行955分以上显然是不正常的,这样的数据记录可能是用户骑行忘记关锁造成的。
针对数据中骑行时长数据存在异常的处理,我们考虑新增骑行速度“speed”一列(骑行路径 / 骑行时长),通过大致剔除骑行速度存在明显异常(骑行速度一般在12-20km/h)的记录而去除掉用户骑行后忘记关锁造成的异常记录。然后再重新绘制直方图观察骑行时长的数据分布情况。
#新增骑行速度,通过剔除骑行速度存在异常的记录处理异常数据,得到新的骑行时长
data['speed'] = data['adderLength'] / (data['lag'] / 60)
data = data[-(((data['speed'] < 12) | (data['speed'] > 20)) )]
lag_data = data["lag"].value_counts().sort_index()
len(lag_data)
lag_c = (
Bar()
.add_xaxis(lag_data.index.tolist())
.add_yaxis("骑行时长",lag_data.values.tolist())
.set_global_opts(title_opts=opts.TitleOpts(title="骑行时长分布图"))
.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
)
lag_c.render_notebook()
如图所示,长尾明显减弱
如图所示,用户的每次骑行时间多集中分布在30分钟之内,我们可以针对这骑行时长分布情况,多多推出30分钟左右的时间满减优惠券。
- 24小时骑行订单数分布分析
hour_data = data["hour"].value_counts().sort_index()
hour_c = (
Bar()
.add_xaxis(hour_data.index.tolist())
.add_yaxis("骑行时长",hour_data.values.tolist())
.set_global_opts(title_opts=opts.TitleOpts(title="骑行时长分布图"))
.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
)
hour_c.render_notebook()
由图可以看出:
23-5点这段时间,人们大多在休息,使用共享单车出行的订单数很少;6点开始,订单数逐渐开始增多;在7-9点、17-20点上下班时段,出现订单数的小高峰,分别是早高峰和晚高峰;11-14点时段,出现局部午高峰,这和中午外出就餐或者休息时间活动有一定关系。
整体趋势表明,共享单车的骑行交通在很大程度上是服务于人们的通勤出行的。
- 工作日和非工作日的24小时骑行订单数分布分析
上面计算的24小时骑行订单数的分布情况忽略了工作日和非工作日的区别,而工作日和非工作日人们的通勤出行情况是有所不同的,那么在工作日和非工作日,骑行订单数的24小时分布情况分别是什么样的呢?
#分别提取非工作日和工作日的订单数据
isweekday_data = data[(data["weekday"]==6) | (data["weekday"]==7)]
noweekday_data = data[~((data["weekday"]==6) | (data["weekday"]==7))]
#找出数据集中包含多少天非工作日的数据和多少天工作日的数据
isweekday_num = isweekday_data["weekday"].value_counts().shape[0]
noweekday_num = noweekday_data["weekday"].value_counts().shape[0]
print("数据集中工作日的天数:" + isweekday_num)
print("数据集中非工作日的天数:" + noweekday_num)
#工作日5天,非工作日2天
#分别计算出工作日、非工作日每个小时的平均订单量
isweekday_hour_data = isweekday_data.groupby("hour").count()["orderid"]/isweekday_num
noweekday_hour_data = noweekday_data.groupby("hour").count()["orderid"]/noweekday_num
isAndNoweekday_hour_c = (
Bar()
.add_xaxis(hour_data.index.tolist())
.add_yaxis("工作日24h订单数分布",noweekday_hour_data.values.tolist())
.add_yaxis("非工作日24h订单数分布",isweekday_hour_data.values.tolist())
.set_global_opts(title_opts=opts.TitleOpts(title="工作日和非工作日的24h订单数分布"))
.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
)
isAndNoweekday_hour_c.render_notebook()
绿色柱状表示工作日内每小时平均订单量的分布。同上述每小时总订单量分布情况相似:23-5点这段时间,使用共享单车出行的订单数很少;6点开始,订单数逐渐开始增多;在7-9点、17-20点上下班时段,出现订单数的小高峰,分别是早高峰和晚高峰;11-14点时段,出现局部午高峰。
紫色柱状为非工作日内每小时平均订单量的分布。非工作日骑行交通以非通勤交通为主,与工作日的分布情况比较,分布相对平缓,没有明显的早晚高峰现象。
空间维度
- 骑行距离(路径)分布分析
#骑行距离单位为千米
adderLength_data = data["adderLength"].value_counts().sort_index()
adderLength_c = (
Bar({"theme": ThemeType.MACARONS})
.add_xaxis([round(i,2) for i in adderLength_data.index.tolist()])
.add_yaxis("骑行距离",adderLength_data.values.tolist())
.set_global_opts(title_opts=opts.TitleOpts(title="骑行距离分布图"))
.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
)
adderLength_c.render_notebook()
骑行距离分布图粗略显示出主要的骑行距离大概集中在3千米左右的范围内。但也存在远距离甚至40公里以上的骑行,可考虑为异常数据;或者可以理解为其他特殊的骑行现象。对数据剔除异常值,首先我们得先识别出异常值。在统计学中,常常用箱型图来判断异常值,箱型图无需对数据进行正态分布要求,适用范围广。而在用箱型图识别异常值之前,我们需要先认识下箱型图。
箱形图因为形状类似一个箱子而得名,它主要用于反映原始数据分布的特征,还可以进行多组数据分布特征的比较。通过箱形图可以很直观的读出一组数据的最大值(上外限)、最小值(下外限)、中位数Q2、上四分位数Q3、下四分位数Q1、异常值(上内限之外和下内限之外的值)。注意图中的上限和下限指的是上内限和下内限,而在绘制箱型图中常常只有上外限(最大值)和下外限(最小值)被显示出来,但我们可以通过公式Q3 + 1.5*IQR计算上内限,通过公式Q1-1.5 * IQR计算下内限。
通过箱型图上下内限划分异常值的数学原理如下:以正态分布为例,假如一组数据服从正态分布, X落在(μ-3σ,μ+3σ)以外的概率小于千分之三,在实际问题中常认为相应的事件是不会发生的(异常值),基本上可以把区间(μ-3σ,μ+3σ)看作是随机变量X实际可能的取值区间,这称之为正态分布的“3σ”(3 sigma)原则。而箱型图的上下内限范围恰好处在(μ-3σ,μ+3σ)之间,由此可以判断处在箱型图上下内限之外的数据为异常值。
我们绘制箱型图,快速判断共享单车的骑行距离数据中的异常值,从而快速去除其异常值。
由图我们观察到绘制出来的箱形图的箱体很扁和下须线非常短,这说明骑行距离的数据按从小到大排序的分布的前75%是十分集中的;而上须线非常长,这说明整个数据集的范围跨度很大,后25%的数据分布十分分散。这一规律也在上面的条形图中反映了出来。
通过图表的提示弹框,我们知道了此箱型图的分布情况:Q1下四分位数3.76、Q3上四分位数12.27,从而计算出IQR四分位距为8.51,上内限为:25.035,下内限为负,我们不予考虑。处于上内限以外的数据是异常值。所以我们考虑把骑行距离大于25.035千米的都视为异常数据,做一层过滤,再绘制骑行距离分布图,从而更清晰地观察骑行距离的分布。
adderLength_data = data[data["adderLength"]<=25.035]["adderLength"].value_counts().sort_index()
adderLength2_c = (
Bar({"theme": ThemeType.MACARONS})
.add_xaxis([round(i,2) for i in adderLength_data.index.tolist()])
.add_yaxis("骑行距离",adderLength_data.values.tolist())
.set_global_opts(title_opts=opts.TitleOpts(title="骑行距离分布图"))
.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
)
adderLength2_c.render_notebook()
可以看出用户使用共享单车的骑行距离(路径)一般在2千米以内,人们对共享单车的需求定位是短途出行,共享单车运营企业可以针对方圆两千米内缺乏大型交通枢纽的居民住宅区、居民娱乐文化区投放共享单车。
- 从衡量每辆共享单车的工作量,找出最可能坏的单车
比较每辆共享单车的工作量可以从三个方面衡量:共享单车的使用次数、共享单车的骑行时间、共享单车的骑行路程。
但从每辆单车的使用次数去衡量单车的工作量,是有失偏颇,这样会忽略掉单车每一次使用的骑行时长因素,有些单车的使用次数多但每一次的骑行时长短,有些单车的使用次数少但每一次的骑行时长长。
从单车的骑行时间去衡量每辆单车的工作量似乎更加公正一点,但这样的前提是数据集已经清除了诸如用户忘记关锁等原因造成的异常记录数据。因为如果有单车被用户使用过后忘记关锁,造成10小时都在“骑行”,在从单车的骑行时间去衡量每辆共享单车的工作量时,这样的单车是第一批被拎出去检修的,但它们的工作量并不是最大的。
相比之下,从骑行路程去衡量每辆单车的工作量会更加公正一点,所以这里选择了从骑行路程去衡量每辆单车的工作量,从而检索出需要被拎出来检修的单车。
这里以骑行时长作为工作量依据,统计每辆单车的骑行路径,筛选出骑行路径前15的车辆ID,作为需要检修的单车名单,当然筛选出的车辆数量也可以自由定义。
#找出骑行路程前15名的单车
workload_data = data.groupby("bikeid").sum()["adderLength"].sort_values(ascending=False).head(15)
workload_data = pd.DataFrame({'bikeid':workload_data.index,'adderLength':workload_data.values})
table = Table()
headers = ["bikeid", "adderLength"]
rows = workload_data.values.tolist()
table.add(headers, rows)
table.set_global_opts(
title_opts=ComponentTitleOpts(title="骑行路程前15的共享单车")
)
table.render_notebook()
- 找出单车紧缺的地区,从而确定新单车的投放地点
找出单车紧缺的地区的分析思路:
选取订单数量最多的8月31日的订单数据作为计算数据
- 计算同样经纬度的位置上的单车迁出数:按照bikeid进行排序,一个bikeid有
多个订单,选取最早时间段的订单数据,对选取的订单数据的起点经纬度进行两次groupby,再调用count()统计同样经纬度的地点上有多少辆单车被骑走。 - 计算同样经纬度的位置上的单车迁入数:按照bikeid进行排序,一个bikeid有
多个订单,选取最晚时间段的订单数据,对选取的订单数据的终点经纬度进行两次groupby,再调用count()统计同样经纬度的地点上有多少辆单车被骑回。 - 计算迁入数减去迁出数的差值,如果某地的单车差值小于0,表示某地的单车“有去无回”。通过统计这些单车数量存在亏损较多的地点多分布在哪个地区,确定新单车的投放地点。
找出单车紧缺的地区的具体分析步骤:
对原有数据的副本进行对经纬度保留两位小数的转换,从而把原有经纬度相差0.01度以内的经纬度都划分为同一经纬度(实际上是在对经纬度分类),扩大统计的地理范围,简化计算的复杂度,在绘图时避免落点堆积。
当然选择保留几位小数是要有考量的,当保留2位小数后得到的经纬度一样时,其地球上真实的经纬度相差不会超过0.01度,地表距离相差不会超过1公里。这个值如果过大(0.1度,10公里)过小(0.001度,100米)都不合适。
#对经纬度保留两位小数
def change_xy(item):
item["start_location_x"] = round(item["start_location_x"],2)
item["start_location_y"] = round(item["start_location_y"],2)
item["end_location_x"] = round(item["end_location_x"],2)
item["end_location_y"] = round(item["end_location_y"],2)
return item
data = data.apply(change_xy,axis = 1)
选取订单数量最多的8月31日的订单数据作为计算数据,按照bikeid进行排序,一个bikeid有
多个订单,选取出最早时间段的订单数据,对选取的订单数据的起点经纬度进行两次groupby,再调用count()统计同样经纬度的地点上有多少辆单车被骑走
data_selected = data[data["date"]==31]
#一辆自行车一天骑行最早的记录
early_order = data_selected.groupby(["bikeid"]).apply(lambda t: t[t.start_time==t.start_time.min()])
out_num = early_order.groupby(["start_location_x","start_location_y"]).count()["bikeid"]
把选取的8月31日数据按照bikeid进行排序,一个bikeid有
多个订单,选取出最晚时间段的订单数据,对选取的订单数据的终点经纬度进行两次groupby,再调用count()统计同样经纬度的地点上有多少辆单车被骑回
#一辆自行车一天骑行最晚的记录
last_order = data_selected.groupby(["bikeid"]).apply(lambda t: t[t.start_time==t.start_time.max()])
#每个区块被骑回来多少单车
in_num = last_order.groupby(["end_location_x","end_location_y"]).count()["bikeid"]
in_num
把数据进行组合得到一个DataFrame(negative_block_data),组合后出现的空值用0填补,计算被骑出去单车数量和被骑回来单车数量的差值,计算出的结果存放在新列“gap”中,筛选出有去无回单车数量达5辆的区块数据
block_out_in_df = pd.DataFrame({"out":out_num,"in":in_num})
block_out_in_df.fillna(0,inplace=True)
block_out_in_df["gap"] = block_out_in_df.apply(lambda item: item["in"]-item["out"],axis = 1)
#找出有去无回单车数量达-5辆的经纬度地区
negative_block_data = block_out_in_df[(block_out_in_df["gap"]) < -4].sort_values(by=["gap"])
目前的问题是negative_block_data还只有经纬度,没有地理位置,我们需要进行“逆地理编码”,把经纬度转换为详细的地理位置。这一步我们选择使用高德地图的API进行转换,但是在使用API前需要注册高德账号申请key(密钥)。
高德开放平台:https://lbs.amap.com/api/webservice/summary,进入控制台—>应用管理—>我的应用—>创建新应用——>…。即会拥有一个专属的key。在调用高德地图的API时,需要附上这个key。
API的URl:https://restapi.amap.com/v3/geocode/regeo,以get的形式向该API获取数据,经纬度信息和key等请求参数放入parameters,一起传送过去。
def parse_loc(location):
location_str=str(location[0])+','+str(location[1])
parameters = {'output': 'json', 'location': location_str, 'key': '42e2546ff2e9a3ac3aa3c001eaebe6ea','extensions':'all'}
base = 'https://restapi.amap.com/v3/geocode/regeo'
response = requests.get(base, parameters)
answer = response.json()["regeocode"]["formatted_address"]
return answer
location = []
for i in negative_block_data.index:
item_result = parse_loc(i)
location.append(item_result)
negative_block_data["location"] = location
#新单车投放地点名单
print(negative_block_data)
#保存成csv文件
negative_block_data.to_csv("./negative_block_data.csv")
加上地理位置后整理出的单车紧缺地区名单(一部分)如图
单车紧缺地区的名单数据包含的数据分别是经度、纬度、某地被骑走的自行车总数、某地寄放的自行车总数、某地缺少的自行车数值以及地名。
整理出单车紧缺地区的名单,我们需要把单车紧缺的地区在地图上标点,绘制地理位置散点图,在这一步中我们采用的是folium。folium是python的一个用来绘制地图,并在地图上打点,画圈,做颜色标记的工具类,使用folium可以制作漂亮的,动态交互的地图。folium还支持交互,比如鼠标点击的地方显示需要展示的内容等等。
安装folium模块命令:
import folium
#数据导入
import pandas as pd
data = pd.read_csv("negative_block_data.csv", encoding='GBK')
print(data)
folium默认绘制的是世界地图,我们也可以指定一个坐标(经纬度),来进行地图中心点初始化,因为是针对上海的摩拜单车数据分析,所以这里指定的是上海中心地标[31.233705, 121.471632]。
#创建地图对象
import folium
from folium import plugins
shanghai_map = folium.Map(location=[31.233705, 121.471632])
folium.Marker([31.233705, 121.471632], popup='<p style="color: green">上海人民广场</p>',icon=folium.Icon(icon='cloud',color='green')).add_to(shanghai_map)
marker_cluster = plugins.MarkerCluster().add_to(shanghai_map)
#互动效果-弹出提示popup,实例中,点击一个点,就会弹出当前点击位置的缺少单车数量以及地点名。
for name,row in data.iterrows():
folium.Marker([row[1], row[0]], popup=folium.Popup('<b>单车数量:</b>{0}<br><b>地点:</b>{1}'.format(int(row["gap"]), row["location"]),max_width=1000),
tooltip='点击查看数据').add_to(marker_cluster)
#保存到本地并用浏览器打开
shanghai_map.save('lackOfBike.html')
注意:
- folium的地理位置散点图更适合作展示,考虑到加载的顺畅性,不建议读取太大的数据。
- 其组件会读外网的jquery,如果所在的网络不能访问google可能效果无法展示。解决办法是把渲染生成的html文件里面的jquery的cdn地址替换成国内的镜像:
<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
换成
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
- 找出单车聚集和紧缺的地区,确定单车调度(调出和调入)的地点
单车紧缺的地区名单已在上面统计了出来,接下来我们需要统计有空闲单车聚集的地区,其统计思路和上面找出单车紧缺的地区,从而确定新单车的投放地点基本一致。
#找出有空闲单车数量达5辆的经纬度地区
positive_block_data = block_out_in_df[(block_out_in_df["gap"]) > 4].sort_values(by=["gap"])
#获取经纬度地区对应的地理位置
location2 = []
for i in positive_block_data.index:
item_result = parse_loc(i)
location2.append(item_result)
positive_block_data["location"] = location2
#保存数据
positive_block_data.to_csv("./positive_block_data.csv")
整理出的有空闲单车聚集地区名单(一部分)如图
接下来我们需要在地图上对单车紧缺和空闲地区进行标点,仍然是采用folium来绘制地理位置散点图。
import pandas as pd
import folium
from folium import plugins
#导入数据
data_negative = pd.read_csv('negative_block_data.csv')
data_positive = pd.read_csv('positive_block_data.csv')
m = folium.Map(location=[31.233705, 121.471632])
folium.Marker([31.233705, 121.471632], popup='<p style="color: green;width:100px;height:50px;font-size:20px" >上海中心点</p>',icon=folium.Icon(icon='cloud',color='blue')).add_to(m)
marker_cluster = plugins.MarkerCluster().add_to(m)
#缺少单车的地点,数量绘制坐标点
for i in range(0,len(data_negative.iloc[:,0])):
x=data_negative.iloc[:,1][i]
y=data_negative.iloc[:,0][i]
test = folium.Html("<b>缺少的单车:{}</b></br><p>地点:{}</p>".format(int(data_negative["gap"][i]),data_negative["location"][i]),script=True)
popup = folium.Popup(test, max_width=1000)
folium.Marker([x,y],popup=popup,icon=folium.Icon(color='red')).add_to(m)
#富余单车的地点,数量和绘制坐标点
for i in range(0,len(data_positive.iloc[:,0])):
x=data_positive.iloc[:,1][i]
y=data_positive.iloc[:,0][i]
test = folium.Html(
"<b>富余的单车:{}</b></br><p>地点:{}</p>".format(int(data_positive["gap"][i]),data_positive["location"][i]),
script=True)
popup = folium.Popup(test, max_width=1000)
folium.Marker([x,y],popup=popup,icon=folium.Icon(color='green')).add_to(m)
m.save('shanghai_map.html')
红点:代表紧缺摩拜共享单车的地点,绿点:代表有摩拜共享单车富余的地点。
由图表我们可以观察到单车的缺少的地点和单车多余的地点,我们可以从有多余单车的地点将单车调度到缺少单车的地点。
用户维度
根据RFM模型进行用户分层,从8个维度进行划分后的用户对应的用户特点如图所示
针对用户类型“user_class”进行统计,统计出各类型用户各有多少人数,绘制出条形图。
user_class_data = data.loc[:,["userid","user_class"]].drop_duplicates()
user_class_num = user_class_data["user_class"].value_counts()
user_c = (
Bar({"theme": ThemeType.MACARONS})
.add_xaxis(user_class_num.index.tolist())
.add_yaxis("各类型用户的数量",user_class_num.values.tolist())
.set_global_opts(title_opts=opts.TitleOpts(title="各类型用户的数量"))
.set_series_opts(label_opts=opts.LabelOpts(is_show=False))
)
user_c.render_notebook()
由图所示,新用户的数量位居第二,和第一名的数量相差无几,可以看出,摩拜共享单车的业绩是处于上升阶段的,我们需要对这些用户时不时推送优惠活动,建立与新用户的联系,使其向忠实用户转化;流失用户的数量位居第三,如何制定用户唤回策略也是摆在摩拜面前的一个关卡。
频次深耕用户、高消费唤回用户和重要价值流失预警用户的数量极少,这说明2016年摩拜单车还是个新企业,其用户结构还不完善。我们可以和有关官方组织合作举办公路自行车马拉松比赛,成为比赛赞助商,提供自行车比赛车辆,从而吸引参赛者更多使用摩拜单车,提高摩拜单车品牌知名度,稳固用户结构。
各个类型用户的平均消费金额如下
从图中可以看到重要价值用户的平均消费金额位居第一,但其用户数量却仅居第四,与前两名还有一段差距,我们需要采取营销策略提高这部分用户的数量,这类用户的用户特点是最近购买、高频、高消费,说明其对共享单车的使用需求高,我们可以在比如工业区等企业聚居密集、居民区等用车频繁的区域投放单车,扩大消费潜力用户的数量。