该数据集包含有关在旧金山湾区的自行车共享系统中的单次骑行信息。本次分析只下载了 2018 年 1 - 12 月的数据。每条信息都是匿名的,其中包括:
- 骑行时长
- 开始时间
- 结束时间
- 开始站点 ID
- 开始站点名称
- 开始站点纬度
- 开始站点经度
- 结束站点 ID
- 结束站点名称
- 结束站点纬度
- 结束站点经度
- 车辆 ID
- 用户类型:
- 会员:"Subscriber" 或者 "Member"
- 散客:"Customer" 或者 "Casual"
- 是否参与 Bike Share for All 计划
一、数据处理
# 加载包
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
pd.set_option('max_colwidth',200)
%matplotlib inline
# 导入数据
df = pd.read_csv('2018-baywheels-tripdata.csv')
df.head()
# 查看数据类型
df.info()
初步清理各列错误的数据类型:
- id 相关列应该是字符串类型;
- time 相关列应该是 datetime 类型;
- bike_share_for_all_trip 应该是布尔值。
# 更改字段类型
column_list = df.columns.tolist()
for c in column_list:
if '_id' in c:
df[c] = df[c].astype(np.int64).astype(str)
elif '_time' in c:
df[c] = pd.to_datetime(df[c])
df.replace(['Yes','No'],[True,False],inplace=True)
# 查看数据类型
df.info()
# 查看缺失值
df.isna().sum()
df.isna().sum()/df.shape[0]
因为这个数据集的量非常大,缺失值比例都比较小,所以直接将所有包含缺失值的数据删除处理。
# 删除缺失值
df.dropna(inplace=True)
df.shape[0]
1851950
二、单变量探索
骑行时长分布
df_copy = df[['duration_sec','start_time','bike_share_for_all_trip','user_type']].copy()
df_copy.duration_sec.hist(bins=50)
plt.xscale('log')
可以看到骑行时长右偏非常严重,使用 log 变换之后也不太容易观察,再详细设置一下:
bin_edges = 10 ** np.arange(1, np.log10(df_copy.duration_sec.max())+0.1, 0.1)
plt.hist(df_copy.duration_sec, bins = bin_edges)
plt.xscale('log')
tick_locs = [60, 180, 300, 600, 1200, 1800, 3600, 7200, 18000]
tick_labels = ['1min','3min','5min','10min','20min','30min','60min','120min','300min']
plt.xticks(tick_locs, tick_labels, rotation=90)
- 可以观察到骑行时长大部分都在 1 小时以下,最多的位于 5-20 分钟的范围内,还是比较合理的。双变量探索时要探索会员与非会员分别的骑行时长分布。
骑行开始时间探索
df_copy['start_date'] = df_copy['start_time'].dt.date
df_copy['start_week'] = df_copy['start_time'].dt.week
df_copy['start_weekday'] = df_copy['start_time'].dt.weekday
df_copy['start_hour'] = df_copy['start_time'].dt.hour
df_copy['start_month'] = df_copy['start_time'].dt.month
df_copy['start_date'].value_counts().sort_index().plot(figsize=(15,5))
plt.xlabel('Start Date');
- 可以看到明显的、有规律的波动,猜测是周末使用量下降导致的,看一下每周使用量是否与上面的波动相似:
df_copy['start_week'].value_counts().sort_index().plot(kind='bar',figsize=(15,5))
- 每周的波动很可能是由于周末导致的,观察一下一周 7 天的变化情况:
df_copy['start_weekday'].value_counts().sort_index().plot(kind='bar',figsize=(10,5))
# dayOfWeek={0:'Monday', 1:'Tuesday', 2:'Wednesday', 3:'Thursday', 4:'Friday', 5:'Saturday', 6:'Sunday'}
weekday_name = ['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday']
plt.xticks(range(0,7), weekday_name)
plt.xlabel('Start Weekday');
- 果然周末使用量减少很多,那么可能使用更多的是通勤的用途,可以探索一下小时的规律,看看能否与通勤挂钩:
df_copy['start_hour'].value_counts().sort_index().plot(kind='bar',figsize=(15,5),rot=0)
plt.xticks(range(0,24), range(0,24))
plt.xlabel('Start Hour')
- 开始骑行的小时,可以发现明显的两个高峰,一个是 8-9 点,一个是 17-18 点,上下班高峰期。
df_copy['start_month'].value_counts().sort_index().plot(kind='bar',figsize=(15,5),rot=0);
- 寒冷的月份使用量较少,5-10月使用量较多。
用户类型分布
print(df_copy.user_type.value_counts())
df_copy.user_type.value_counts().plot(kind='pie', autopct='%.1f%%',figsize=(5,5))
Subscriber 1574991
Customer 276959
- 大部分骑行订单来自会员账户,只有 15% 来自散客。
Bike Share for All 服务
Bike Share for All 这是一种服务方式,根据官网的介绍,似乎针对的是低收入人群,需要符合一些标准才可以申请.
df_copy.bike_share_for_all_trip.value_counts().plot(kind='pie', autopct='%.1f%%',figsize=(5,5))
- 只有 8.7% 的用户享用 Bike Share for All 服务。
三、双变量探索
会员 vs 骑行时长
df_copy.boxplot(column='duration_sec',by='is_vip',showfliers=False, showmeans=True,figsize=(7,5))
plt.ylabel('duration_sec');
df_copy.groupby('is_vip')['duration_sec'].describe()
- 从上面的箱线图和描述性统计信息可以看到,会员的骑行时长相对比较短,绝大多数都在 1500 秒以下,也就是小于 25 分钟。75% 的人骑行时长都在 13 分钟以下。
def duration_hist(df,label):
bin_edges = 10 ** np.arange(1, np.log10(df_copy.duration_sec.max())+0.1, 0.1)
df['duration_sec'].hist(bins=bin_edges,alpha=0.5,figsize=(7,5),label=label)
plt.xscale('log')
tick_locs = [60, 180, 300, 600, 1200, 1800, 3600, 7200, 18000]
tick_labels = ['1min','3min','5min','10min','20min','30min','60min','120min','300min']
plt.xticks(tick_locs, tick_labels, rotation=90)
plt.xlabel('Duration')
plt.ylabel('Number of users')
plt.title('Duration vs Vip vs Count')
plt.legend()
duration_hist(df_copy[df_copy['is_vip']==True],'VIP')
duration_hist(df_copy[df_copy['is_vip']==False],'NotVIP')
- 通过上方直方图对比,可以看到非会员的分布更加均匀且时长偏长,会员的时间非常集中在 3-20 分钟的范围内
bike share for all服务 vs 会员
def variable_visulization(df, feature):
'''各个因素对注册会员的影响分析对比柱状图组合'''
f, ax = plt.subplots(1,2,figsize=(18,5))
# 左侧显示注册会员人数对比柱状图
df1 = df.groupby([feature,'is_vip'])['is_vip'].count().unstack()
df1.plot(kind='bar',ax=ax[0])
ax[0].set_ylabel('Number of trips')
ax[0].set_title(feature + ' VS Vip VS Count')
# 右侧显示注册会员几率柱状图
df2 = df.groupby(feature)['is_vip'].mean()
df2.plot(kind='bar',ax = ax[1])
ax[1].set_ylabel('Vip Rate')
ax[1].set_title(feature + ' VS Vip Rate')
for i, mean in enumerate(df2):
ax[1].text(i+0.08, mean-0.03, round(mean, 3),
horizontalalignment='center',rotation=90,color='white')
plt.show()
variable_visulization(df_copy,'bike_share_for_all_trip')
- bike share for all 服务全部属于会员,可能是因为该服务也属于会员的一种。
会员 vs 周
variable_visulization(df_copy,'start_weekday')
- 可以看到周末和工作日的表现呈现两种形式,创建一个新的变量 is_weekend,看看周末与非周末的直观差异:
df_copy['is_weekend'] = df_copy['start_weekday'].isin([5,6])
variable_visulization(df_copy,'is_weekend')
- 上图中可以看到,共享单车的使用,会员在工作日使用更多,非会员在周末使用更多,工作日的会员订单比例更多。这也比较符合认知,如果一个人通勤时需要使用共享单车,注册会员可能会更加划算;而周末使用的人很可能是偶然情况下使用。
会员 vs 月
variable_visulization(df_copy,'start_month')
- 单变量探索中,发现的规律是寒冷的月份使用的比较少,这在上面左侧的图表中同样可以体现出来。每个月份的会员使用比例差异不是特别大,不过在相对寒冷的 1-4 月和 10-12 月,会员订单的比例是相对较高的,在寒冷的月份,会员仍有选择骑车的倾向,或许是习惯,或许是已经享受了订阅服务,不使用有点浪费?
会员 vs 时
variable_visulization(df_copy,'start_hour')
- 可以看到共享单车会员的使用高峰与探索单变量时得出的结论一致,都是上下班高峰,但是非会员的分布就比较平滑,没有那么明显的差异。在右边的图表中也可以看出,会员在早晚高峰的使用率也是较多的。
是否周末 vs 骑行时长
- 因为骑行时长与会员存在一定的相关性,而会员和是否为周末也存在一定的关系,所以再来看看骑行时长与周末是否有关系,先来看看箱线图的表现:
df_copy.boxplot(column='duration_sec',by='start_weekday',showfliers=False, showmeans=True,figsize=(7,5))
plt.ylabel('duration_sec')
df_copy.boxplot(column='duration_sec',by='is_weekend',showfliers=False, showmeans=True,figsize=(7,5))
plt.ylabel('duration_sec')
duration_hist(df_copy[df_copy['is_weekend']==False],'Weekday')
duration_hist(df_copy[df_copy['is_weekend']==True],'Weekend')
- 图中可以看到,工作日的骑行时长处于 5-20 min 的数据非常多,周末的数据分布则更为平均。
是否周末 vs 骑行开始时间
因为骑行时长与会员存在一定的相关性,而会员和是否为周末也存在一定的关系,所以再来看看骑行时长与周末是否有关系,先来看看箱线图的表现:
df_copy.groupby(['start_hour','is_weekend']).size().unstack().plot(figsize=(10,5))
plt.title('is_weekend vs start_hour')
plt.ylabel('Number of trips');
- 可以看到周末的数据非常平滑,是按照白天较多、夜晚较少的分布,而工作日正如想象的那样呈现了两个峰值:上下班的高峰期。
小结
- 会员的骑行时长相对更短,非会员的分布更加均匀且时长偏长,会员的时间非常集中在 3-20 分钟的范围内;
- 会员骑行更集中在工作日、通勤时间段,在寒冷的月份,比起非会员来说更有选择骑车的可能;
- 工作日的订单骑行时长更集中、更短,周末的骑行时长更为分散,相对时间较长。
- 工作日的订单更多发生在早晚高峰,周末白天的时间都比较均匀。
四、多变量探索
这部分主要想探索的是会员与骑行时长和骑行发生的时间(是否为周末、通勤时间等)的关系,先创建一个可复用的函数:
def muliple_features(x,y="duration_sec",hue="is_vip"):
plt.figure(figsize=(10,5))
sns.boxplot(x=x, y=y, hue=hue,
data=df_copy, palette="Set3",showfliers=False, showmeans=True)
plt.title('{} vs {} vs {}'.format(x,y,hue))
会员 vs 骑行时长 vs 周
muliple_features("start_weekday")
muliple_features("is_weekend")
- 通过上面两幅图表,明显可以看出,绿色的箱线更分散,均值更靠上。可以判断会员和非会员周末的骑行时长都相对更长,工作日都相对较短,而会员不论是周末还是工作日都比非会员的骑行时长更为集中。
muliple_features("start_hour")
- 骑行开始时间,可以从均值看出夜晚和凌晨的时间波动比较大,会员的骑行时长一直都相对稳定,非会员在白天 10-16 点的时间段骑行时长比较长。
VIP vs Start Hour vs Weekend vs Duration 折线图
下面将最感兴趣的四个变量放到一个图表中进行观察,这里选择了 seaborn 库的绘图方式,一方面是代码简练,另一方面还可以看到 95% 置信区间,观察到波动剧烈的凌晨数据。
g = sns.relplot(x="start_hour", y="duration_sec", hue="is_vip", col="is_weekend",
height=5, aspect=12/7, facet_kws=dict(sharex=False),
kind="line", legend="full", data=df_copy)
g.fig.suptitle('VIP vs Start Hour vs Weekend vs Duration', fontsize=12);
- 图中可以观察到,会员的折线波动都非常平滑,集中在较短的时间内,骑行时长的均值都维持在 1000 左右,也就是约 16 分钟左右。非会员的骑行时长均值维持在会员的折线上方,且波动较大,置信区间的范围也较大,说明数据较为分散。工作日和周末的对比不是特别明显,再绘制一份柱状图观察一下:
g = sns.catplot(x="start_hour", y="duration_sec", hue="is_weekend", col="is_vip",
data=df_copy, kind="bar", height=5, aspect=12/7)
g.fig.suptitle('VIP vs Start Hour vs Weekend vs Duration',fontsize=12);
上图可以看到:
- 夜晚和凌晨的误差条都比较长,说明数据较为分散,没有那么明确的规律;
- 左右两个子图对比,可以看出左侧的长条都相对较高,说明非会员的骑行时长比会员的骑行时长要更长;
- 右侧会员的数据中,除了凌晨的个别时间段,基本上都处于 1000 以下,且误差条非常短,说明骑行时长集中,且时长都较短;
- 除了凌晨的个别时间段,基本上所有的橙色长条都比蓝色长条要高,说明基本上周末都比工作日的骑行时长要长;