出于报告呈现的整洁度和美观度考虑,本篇仅展现分析背景、分析结论和分析过程,前期数据集清洗整理和信息提取,以及探索性分析和可视化过程可移步:《摩拜单车数据探索与可视化》
在数据探索过程中,发现骑行时间(包括工作日/双休日、高峰时段/非高峰时段)、骑行区域和用户价值这四个变量均会对骑行时长产生影响。当依次限定四个变量条件后,可以发现:
在关注类型变量对于骑行时长的影响之外,又有以下发现:
针对上述用户行为数据分析结论,提出以下优化建议:
因原始数据集中的信息量过少,可供分析的内容有限,扩大数据集信息和范围后,可用于分析的内容包括但不限于:
主要关注四个类型变量对于骑行时长的影响。首先介绍骑行时长、骑行距离的数据分布情况。然后通过绘制小提琴图,观察到两者随类型变量的变化而高度相似的数据特征,进而判定只需关注骑行时长这一关键指标和四个类型变量之间的关系即可。最后通过点图绘制在控制不同条件的类型变量的情况下,其他变量对于骑行时长的影响。
# 导入所有需要用到的库,并将图表设置为直接显示
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sb
%matplotlib inline
# 清除输出中的警告
import warnings
warnings.simplefilter("ignore")
# 导入清洗整理后的数据集
df_e = pd.read_csv('mobike_master.csv')
# 将对应列转换成类别变量
order_dict = {'ring_stage': ['inside inner ring', 'inside middle ring', 'inside outer ring', 'outside outer ring'],
'rate': ['high-value user', 'middle-value user', 'low-value user'],
'daytype': ['weekdays', 'weekends'],
'hourtype': ['rush hours', 'non-rush hours']}
for var in order_dict:
order = pd.api.types.CategoricalDtype(ordered = True, categories = order_dict[var])
df_e[var] = df_e[var].astype(order)
# 数据清理,去除少量在骑行速度、骑行时长和骑行距离上明显异常的数据
df_e['speed'] = df_e['distance'] / (df_e['ttl_min'] / 60)
df_e = df_e[-(((df_e['speed'] < 12) | (df_e['speed'] > 20)) & ((df_e['ttl_min'] > 720) | (df_e['distance'] > 50)))]
骑行时长的数据范围非常大,最小值为1分钟,最大值为666分钟,且呈现长尾分布,绝大多数骑行时长较短,使用log转换x轴绘图可以发现,骑行时长呈现右偏态分布,峰值出现在7-10分钟之间。
bins = 10 ** np.arange(0, np.log10(df_e.ttl_min.max()) + 0.15, 0.15)
plt.hist(data = df_e, x = 'ttl_min', bins = bins);
plt.xscale('log')
xticks = (1, 2, 5, 10, 20, 50, 100, 200, 500)
plt.xticks(xticks, xticks);
plt.xlabel('Riding Duration (min)');
plt.title('Distribution of Riding Duration');
数据集中的骑行距离用的是骑行始末点之间的直线距离作为估算的,该数据的范围同样非常大,最小值为0.146km,最大值为32.497km,且同样呈现长尾分布,绝大多数骑行距离较短,极少数骑行距离较长,使用log转换x轴绘图可以发现,骑行距离呈现右偏态分布,峰值出现在0.7-1.3公里之间。
bins = 10 ** np.arange(np.log10(df_e.distance.min()), np.log10(df_e.distance.max()) + 0.08, 0.08)
plt.hist(data = df_e, x = 'distance', bins = bins);
plt.xscale('log')
xticks = (0.1, 0.2, 0.5, 1, 2, 5, 10, 20)
plt.xticks(xticks, xticks);
plt.xlabel('Riding Distance (km)');
plt.title('Distribution of Riding Distance');
通过比较,发现骑行时长和骑行距离这两个数值变量的数据分布特征和分类情况下的变化特征几乎完全一样,后续可以不再对骑行距离做分析,原因如下:一方面骑行时长是原始数据中的真实数据(骑行距离是通过骑行始末点估算的直线距离),另一方面摩拜单车是根据骑行时长作为付费依据的,故此,在数据特征高度相似的情况下,选择数据质量和价值更高的骑行时长作为后续分析指标。
# 由于骑行时长和骑行距离的数据均呈现非常长的长尾,所以先对两个数据进行log转化,以便更清晰地观察数据特征
df_e['log_ttl_min'] = np.log10(df_e['ttl_min'])
df_e['log_distance'] = np.log10(df_e['distance'])
cat_vars = ['daytype', 'hourtype', 'ring_stage', 'rate']
fig, ax = plt.subplots(ncols = 4, nrows = 2, figsize = [20,10])
color = sb.color_palette()[0]
for i in range(len(cat_vars)):
var = cat_vars[i]
# 画第一行的图
sb.violinplot(data = df_e, x = var, y = 'log_ttl_min', ax = ax[0, i], color = color);
ttl_min_ticks = [1, 2, 5, 10, 20, 50, 100, 200, 500]
ax[0, i].set_yticks(np.log10(ttl_min_ticks));
ax[0, i].set_yticklabels(ttl_min_ticks);
ax[0, i].set_ylabel('Riding Duration (min)');
if i == 2:
xlabels = ax[0, i].get_xticklabels()
ax[0, i].set_xticklabels(xlabels, rotation = 10);
# 画第二行的图
sb.violinplot(data = df_e, x = var, y = 'log_distance', ax = ax[1, i], color = color);
distance_ticks = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20]
ax[1, i].set_yticks(np.log10(distance_ticks));
ax[1, i].set_yticklabels(distance_ticks);
ax[1, i].set_ylabel('Riding Distance (km)');
if i == 2:
xlabels = ax[1, i].get_xticklabels()
ax[1, i].set_xticklabels(xlabels, rotation = 10);
plt.suptitle('riding duration and distance by other features', fontsize = 'xx-large');
# 自定义函数,绘制控制变量条件下的pointplot图
def ppltgrid(row_dict):
for var in row_dict:
firstplot = list(row_dict.keys())[0] # 设置第一个绘图的编号,以便后续获取第一个绘图y轴的操作
a0,b0,c0 = var.split(',')
a,b,c = int(a0), int(b0), int(c0)
plt.subplot(a,b,c)
flagid, flag, hue, x = row_dict[var]['flagid'], row_dict[var]['flag'], row_dict[var]['hue'], row_dict[var]['x']
ax = sb.pointplot(data = df_e[df_e[flagid] == flag], x = x, y = 'log_ttl_min', hue = hue,
palette = 'Blues_r', linestyles = '', dodge = 0.1);
ax.set_title("{}'s riding duration across {} and {}".format(flag, x, hue), fontsize = 'small');
ylocs = np.arange(1, 1.25, 0.025)
ylabels = np.round(np.power(10, ylocs), 2)
ax.set_yticks(ylocs);
ax.set_yticklabels(ylabels);
ax.set_yticklabels([],minor = True); # 不显示默认的主要刻度
if c % b == 1: # 为每行的第一个图设置y轴标签,其他图则不显示,以防遮盖图表内容
ax.set_ylabel('Mean Riding Duration (min)');
else:
ax.set_ylabel('');
if x == 'ring_stage' or x == 'rate': # ring_stage和rate的类别名称过长,将字体变小
xlabels = ax.get_xticklabels()
ax.set_xticklabels(xlabels, fontsize = 'small');
if var == firstplot:
ylim = ax.get_ylim() # 获取第一个绘图的y轴
else:
plt.ylim(ylim); # 使第二个开始的所有绘图保持和第一个绘图一致的y轴范围
plt.figure(figsize = [15, 10])
row_dict = {'2,2,1': {'flagid': 'daytype', 'flag': 'weekdays', 'hue': 'rate', 'x': 'ring_stage'},
'2,2,2': {'flagid': 'daytype', 'flag': 'weekends', 'hue': 'rate', 'x': 'ring_stage'},
'2,2,3': {'flagid': 'hourtype', 'flag': 'rush hours', 'hue': 'rate', 'x': 'ring_stage'},
'2,2,4': {'flagid': 'hourtype', 'flag': 'non-rush hours', 'hue': 'rate', 'x': 'ring_stage'}}
ppltgrid(row_dict)
plt.figure(figsize = [20,10])
row_dict = {'2,4,1': {'flagid':'ring_stage', 'flag': 'inside inner ring', 'hue': 'daytype', 'x': 'hourtype'},
'2,4,2': {'flagid':'ring_stage', 'flag': 'inside middle ring', 'hue': 'daytype', 'x': 'hourtype'},
'2,4,3': {'flagid':'ring_stage', 'flag': 'inside outer ring', 'hue': 'daytype', 'x': 'hourtype'},
'2,4,4': {'flagid':'ring_stage', 'flag': 'outside outer ring', 'hue': 'daytype', 'x': 'hourtype'},
'2,4,5': {'flagid':'rate', 'flag': 'high-value user', 'hue': 'daytype', 'x': 'hourtype'},
'2,4,6': {'flagid':'rate', 'flag': 'middle-value user', 'hue': 'daytype', 'x': 'hourtype'},
'2,4,7': {'flagid':'rate', 'flag': 'low-value user', 'hue': 'daytype', 'x': 'hourtype'}}
ppltgrid(row_dict)