摩拜共享单车数据分析项目报告 |
您所在的位置:网站首页 › 摩拜单车app开锁用车功能测试报告 › 摩拜共享单车数据分析项目报告 |
文章目录
项目背景数据探索数据挖掘数据分析时间维度空间维度用户维度
项目背景
随着智能手机的普及和手机用户的激增,共享单车作为城市交通系统的一个重要组成部分,以绿色环保、便捷高效、经济环保为特征蓬勃发展。共享单车企业通过在校园、公交站点、居民区、公共服务区等提供服务,完成交通行业最后一块“拼图”,与其他公共交通方式产生协同效应。共享单车有助于缓解城市短距离交通出行和“最后一公里”难题,但共享单车由于其运营特点,对企业在城市投放和调度单车的规划管理方面,存在较大挑战。 基于上述背景,本文基于上海摩拜单车的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())查看数据集的空值分布情况,数据不存在空值 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”列数据得到骑行路径。 把骑行轨迹字符串按“#”分隔符分隔成列表,得到骑行轨迹的经纬度信息列表 租赁时间 => 星期几 + 时间段(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类 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模型中打分一般采取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)
骑行起点终点经纬度 => 骑行起点终点所处的区块 在这一步中我们把骑行起点终点的经纬度转换为GeoHash编码字符串。 GeoHash将二维的经纬度转换成GeoHash编码字符串,每一个字符串代表了某一矩形区域。这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串。GeoHash本质上是一个哈希函数,但和其他的一些哈希函数会尽力避免哈希碰撞不一样,GeoHash根据经纬度范围把不同的经纬度哈希碰撞到同一字符串。 Geohash能够提供任意经纬度的分段级别,一般分为1-12级。Geohash编码字符串越长,表示的区域范围越精确,GeoHash编码字符串长度所对应的区块范围。 在转换之前需要安装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()
针对数据中骑行时长数据存在异常的处理,我们考虑新增骑行速度“speed”一列(骑行路径 / 骑行时长),通过大致剔除骑行速度存在明显异常(骑行速度一般在12-20km/h)的记录而去除掉用户骑行后忘记关锁造成的异常记录。然后再重新绘制直方图观察骑行时长的数据分布情况。 #新增骑行速度,通过剔除骑行速度存在异常的记录处理异常数据,得到新的骑行时长 data['speed'] = data['adderLength'] / (data['lag'] / 60) data = data[-(((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()如图所示,长尾明显减弱
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()
紫色柱状为非工作日内每小时平均订单量的分布。非工作日骑行交通以非通勤交通为主,与工作日的分布情况比较,分布相对平缓,没有明显的早晚高峰现象。 空间维度 骑行距离(路径)分布分析 #骑行距离单位为千米 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()
箱形图因为形状类似一个箱子而得名,它主要用于反映原始数据分布的特征,还可以进行多组数据分布特征的比较。通过箱形图可以很直观的读出一组数据的最大值(上外限)、最小值(下外限)、中位数Q2、上四分位数Q3、下四分位数Q1、异常值(上内限之外和下内限之外的值)。注意图中的上限和下限指的是上内限和下内限,而在绘制箱型图中常常只有上外限(最大值)和下外限(最小值)被显示出来,但我们可以通过公式Q3 + 1.5*IQR计算上内限,通过公式Q1-1.5 * IQR计算下内限。
通过图表的提示弹框,我们知道了此箱型图的分布情况:Q1下四分位数3.76、Q3上四分位数12.27,从而计算出IQR四分位距为8.51,上内限为:25.035,下内限为负,我们不予考虑。处于上内限以外的数据是异常值。所以我们考虑把骑行距离大于25.035千米的都视为异常数据,做一层过滤,再绘制骑行距离分布图,从而更清晰地观察骑行距离的分布。 adderLength_data = data[data["adderLength"]我的应用—>创建新应用——>…。即会拥有一个专属的key。在调用高德地图的API时,需要附上这个key。![]() 加上地理位置后整理出的单车紧缺地区名单(一部分)如图 整理出单车紧缺地区的名单,我们需要把单车紧缺的地区在地图上标点,绘制地理位置散点图,在这一步中我们采用的是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='上海人民广场 ',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('单车数量:{0}地点:{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地址替换成国内的镜像:换成 单车紧缺的地区名单已在上面统计了出来,接下来我们需要统计有空闲单车聚集的地区,其统计思路和上面找出单车紧缺的地区,从而确定新单车的投放地点基本一致。 #找出有空闲单车数量达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")整理出的有空闲单车聚集地区名单(一部分)如图 上海中心点 ',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("缺少的单车:{}地点:{} ".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( "富余的单车:{}地点:{} ".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个维度进行划分后的用户对应的用户特点如图所示
频次深耕用户、高消费唤回用户和重要价值流失预警用户的数量极少,这说明2016年摩拜单车还是个新企业,其用户结构还不完善。我们可以和有关官方组织合作举办公路自行车马拉松比赛,成为比赛赞助商,提供自行车比赛车辆,从而吸引参赛者更多使用摩拜单车,提高摩拜单车品牌知名度,稳固用户结构。 各个类型用户的平均消费金额如下 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |