共享单车数据分析

该数据集包含有关在旧金山湾区的自行车共享系统中的单次骑行信息。本次分析只下载了 2018 年 1 - 12 月的数据。每条信息都是匿名的,其中包括:

  • 骑行时长
  • 开始时间
  • 结束时间
  • 开始站点 ID
  • 开始站点名称
  • 开始站点纬度
  • 开始站点经度
  • 结束站点 ID
  • 结束站点名称
  • 结束站点纬度
  • 结束站点经度
  • 车辆 ID
  • 用户类型:
    1. 会员:"Subscriber" 或者 "Member"
    2. 散客:"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 以下,且误差条非常短,说明骑行时长集中,且时长都较短;
  • 除了凌晨的个别时间段,基本上所有的橙色长条都比蓝色长条要高,说明基本上周末都比工作日的骑行时长要长;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342