1. Bike Sharing Analysis
在这章主要介绍如何分析共享单车服务数据,以及如何基于时间、天气状态特征来识别单车的使用模式。除此之外,我们还会引入可视化分析,假设检验、以及时间序列分析的概念与方法。
共享单车是城市里较为快速的通勤方式,了解用户使用共享单车所考虑的因素,对于公司和用户来说都是必须的。
从公司的角度来看,了解某一个时间段某一区域里,用户对共享单车的需求,可以显著地提升业绩以及用户满意度。同时也可以优化未来的运营成本。从用户的角度来看,可能最重要的因素是:在最短的时间内满足对单车的需求。这点与公司的利益是一致的。
在这篇文章中,我们会分析来自于华盛顿Capital Bikeshare 的单车共享数据,时间跨度从2011年1月1日,到2012年12月31日,数据以小时级别进行了聚合。也就是说, 数据中不包含单次骑行的起始与终止的位置,而是仅仅每小时的骑行次数。除此之外,数据集中还有额外的天气信息,可作为一个影响因素,影响在某个特定时间点对骑行的需求总数(天气比较差的时候可能会对骑行需求有较大的影响)。
1.1. Note
源数据获取地址:https://archive.ics.uci.edu/ml/datasets/Bike+Sharing+Dataset#
若是对此topic 比较感兴趣,可以进一步阅读论文:Fanaee-T, Hadi, and Gama, Joao, 'Event labeling combining ensemble detectors and background knowledge', Progress in Artificial Intelligence (2013): pp. 1-15, Springer Berlin Heidelberg.
虽然这个主题仅是对共享单车进行分析,但是提供的技术可以很容易应用到其他不同的共享商业模型,例如共享汽车或是共享摩托车等。
2. 理解数据
我们首先加载数据并做初始的分析。这里的目标主要是:
- 对数据有基本的了解
- 各个特征是如何分布
- 是否有缺失值
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
import s3fs
%matplotlib inline
# load hourly data
hourly_data = pd.read_csv('s3://tang-sagemaker/workshop/bike_sharing/hour.csv')
print(f"Shape of data: {hourly_data.shape}")
print(f"Number of missing values in the data: {hourly_data.isnull().sum().sum()}")
Res:
Shape of data: (17379, 17)
Number of missing values in the data: 0
查看一下统计数据:
结合Readme.txt 里对数据的说明,我们可以了解以下几点:
- instant 为 id 索引,对预测无帮助
- 离散型特征有:season,yr,mnth,hr,holiday,weekday,workingday,weathersit
- 数值型特征有:temp,atemp,hum,windspeed,casual,registered,cnt
- 数值型特征中temp、atemp,hum,windspeed 均已做标准化处理,casual,registered,cnt未做标准化处理
- cnt 是 casual 与 registered 相加之和,可以由这两个属性计算出
- dteday为时间特征
按照特征描述分类,可以分为3大类:
- 时间相关,包含条目注册时的时间:dteday,season,yr,mnth,hr,holiday,weekday,workingday
- 天气相关,包含天气条件:weathersit,temp,atemp,hun以及windspeed
- 条目本身相关,包含指定小时内,总records数:casual,registered以及cnt
3. 数据预处理
为了适应机器学习算法的需求,使预测结果更为准去,我们需要对数据做预处理。偶尔也会为了可视化的目的进行预处理展示。
3.1. 处理时间与天气特征
这里对时间与天气特征进行处理,主要不是为了方便机器学习训练,而是为了方便人可读。在数据集中,有部分特征已经被编码过,我们再次将这些特征进行编码,方便人可读:
- season 特征,它的值为1到4,分别对应的是 Spring、Summer、Fall和 Winter;
- yr特征,它的值为 0和1,分别对应2011 和 2012;
- weekday特征,值为0到6,分别对应一周的每天(0: Sunday,6: Saturday)
- weathersit特征,值为1到4,分别对应的是clear, cloudy, light_rain_snow, heavy_rain_snow
- hum特征,被缩放到了0到1区间内,原始应为0到100区间内
- windspeed特征,被缩放到了0到1区间内,原始应为0到67区间内
首先处理 season、yr以及weekday 特征:
preprocessed_data = hourly_data.copy()
# temperal features
seasons_map = {1:"Spring", 2:"Summer", 3:"Fall", 4:"Winter"}
yr_map = {0:2011, 1:2012}
weekday_mapping = {0:'Sunday', 1:'Monday', 2:'Tuesday', 3:'Wednesday', 4:'Thursday', 5:'Friday', 6:'Saturday'}
preprocessed_data['season'] = preprocessed_data['season'].apply(lambda x: seasons_map[x])
preprocessed_data['yr'] = preprocessed_data['yr'].apply(lambda x: yr_map[x])
preprocessed_data['weekday'] = preprocessed_data['weekday'].apply(lambda x: weekday_mapping[x])
继续处理weathersit、hum以及windspeed特征。其中hum以及wind的原始范围分别为 [0, 100] 以及[0, 67],已经被缩放为[0, 1]:
# weather features
weather_mapping = {1: 'clear', 2: 'cloudy', \
3: 'light_rain_snow', 4: 'heavy_rain_snow'}
preprocessed_data['weathersit'] = preprocessed_data['weathersit'].apply(lambda x: weekday_mapping[x])
preprocessed_data['hum'] = preprocessed_data['hum'] * 100
preprocessed_data['windspeed'] = preprocessed_data['windspeed'] * 67
验证转换效果:
# validate
cols = ['season', 'yr', 'weekday', 'weathersit', 'hum', 'windspeed']
preprocessed_data[cols].sample(10, random_state=42)
3.2. Registered versus Casual分析
根据数据说明,registered + casual = cnt,我们可以验证一下:
assert (preprocessed_data['registered'] + preprocessed_data['casual'] == preprocessed_data['cnt']).all(), 'not all are equal'
首先对这2个特征进行分析的话,可以看一下它们的分布,这里会使用到seaborn,它是基于标准matplotlib构建的可视化库,为不同的统计图提供了更高级的接口。下面我们看一下registered 与 casual 骑行的分布:
# plot distribution of registered and casual
sns.distplot(preprocessed_data['registered'], label='registered')
sns.distplot(preprocessed_data['casual'], label='casual')
plt.legend()
plt.xlabel('rides')
plt.title("Rides Distribution")
plt.savefig('figs/rides_distributions.png', format='png')
从分布图我们可以了解到:
- 两者的分布均为正倾斜
- 骑行的registered 用户远多于casual 用户
下面我们探索一下随时间变化的骑行数,以天为单位:
# plot evolotion of ride over time
plot_data = preprocessed_data[['registered', 'casual', 'dteday']]
ax = plot_data.groupby('dteday').sum().plot(figsize=(10, 6))
ax.set_xlabel("time")
ax.set_ylabel("number of rides per day")
plt.savefig('figs/rides_daily.png', format='png')
从这个图可以看到:
- registered 用户的骑行次数,基本每天都是要明显超出casual 用户非常多
- 冬季骑行数会少下降
但是这个图中,两个时间之间的差比非常大,所以有很高的的抖动(毛刺)。有一个平滑毛刺的方法是:使用滚动平均值与滚动标准差来替换所需要可视化的值,以及它们的期望标准差情况。
plot_data = preprocessed_data[['registered', 'casual', 'dteday']]
plot_data = plot_data.groupby('dteday').sum()
# define window for computing the rolling mean and standard deviation
window = 7
rolling_means = plot_data.rolling(window).mean()
rolling_deviation = plot_data.rolling(window).std()
ax = rolling_means.plot(figsize=(10, 6))
ax.fill_between(rolling_means.index,
rolling_means['registered'] + 2*rolling_deviation['registered'],
rolling_means['registered'] - 2*rolling_deviation['registered'],
alpha=0.2)
ax.fill_between(rolling_means.index,
rolling_means['casual'] + 2*rolling_deviation['casual'],
rolling_means['casual'] - 2*rolling_deviation['casual'],
alpha=0.2)
ax.set_xlabel("time")
ax.set_ylabel("number of rides per day")
plt.savefig('figs/rides_aggregated.png', format='png')
下面我们继续关注一下骑行请求随一天中不同小时、以及一周中不同天的分布情况。我们预期是会有随时间变化的骑行请求数,因为直觉来看,骑行的请求数应该在一天中某几个特定小时,以及一周中的特定天是有关的。
# select relevant columns
plot_data = preprocessed_data[['hr', 'weekday', 'registered', 'casual']]
plot_data = plot_data.melt(id_vars=['hr', 'weekday'], var_name='type', value_name='count')
grid = sns.FacetGrid(plot_data, row='weekday', col='type', height=2.5, aspect=2.5,
row_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'])
grid.map(sns.barplot, 'hr', 'count', alpha=0.5)
grid.savefig('figs/weekday_hour_distributions.png', format='png')
从这个图我们可以了解到:
- 在工作日,用户骑行时间主要分布在早上8点到下午6点之间,符合我们的预期;
- Registered 用户为共享单车的主要使用者
- Casual 用户在工作日使用共享单车有限
- 在休息日,可以明显看到对于registered 与 casual 用户骑行的分布有变化,但registered 用户仍占主要使用者大部分;两者的分布基本一致,在早上11点AM 到 6点PM的分布类似于均匀分布
总的来说,我们可以得出结论:大部分的单车使用在工作日,一般为工作时间内(如早9晚5)。
3. 天气对骑行影响分析
下面我们继续探索天气对骑行的影响。
plot_data = plot_data.melt(id_vars=['hr', 'season'], var_name='type', value_name='count')
grid = sns.FacetGrid(plot_data, row='season', col='type', height=2.5, aspect=2.5,
row_order = ['Spring', 'Summer', 'Fall', 'Winter'])
grid.map(sns.barplot, 'hr', 'count', alpha=0.5)
从四个季度来看,分布基本一致,其中春季的骑行需求稍低。
再从weekday方面进一步探索:
plot_data = preprocessed_data[['weekday', 'season', 'registered', 'casual']]
plot_data = plot_data.melt(id_vars=['weekday', 'season'], var_name='type', value_name='count')
grid = sns.FacetGrid(plot_data, row='season', col='type', height=2.5, aspect=2.5,
row_order = ['Spring', 'Summer', 'Fall', 'Winter'])
grid.map(sns.barplot, 'weekday', 'count', alpha=0.5,
order=['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'])
从这个图我们可以看到:对于registered 用户来说,工作日使用量高于休息日使用量;对于casual 用户来说,休息日使用量高于工作日使用量。
据此,我们可能会提出初始的假设:registered 用户用共享单车主要是为了通勤,而casual用户主要在周末偶尔使用共享单车。
当然,这个假设结论不能仅基于可视化图像观察,还需要有背后的统计测试进行支持。也就是我们下一节要讨论的问题。