前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >pandas实战:出租车GPS数据分析

pandas实战:出租车GPS数据分析

作者头像
Python数据科学
发布2023-08-29 19:29:53
6270
发布2023-08-29 19:29:53
举报
文章被收录于专栏:Python数据科学Python数据科学

上次分享了电商行业的项目实战:pandas实战:电商平台用户分析

本次分享一个交通行业实战项目,这个项目是对出租车GPS数据进行分析,具体内容包括了数据理解、业务场景、数据处理、可视化等。

一、数据处理

数据表的变量含义如下。

  • id:车辆编号,唯一标识
  • time:GPS采集时间
  • long:GPS经度
  • lati:GPS纬度
  • status:载客状态,1为载客,0为空客
  • speed:采集的GPS车速

首先读取数据,由于原数据没有header,直接就是数据,因此需设置为None,然后手动添加列索引名称。

代码语言:javascript
复制
# 读取数据
hd = ['id','time','long','lati','status','speed'] 
df = pd.read_csv('taxi.csv', header=None, names=hd)   

数据来源:《交通时空大数据分析、挖掘与可视化》

看下数据整体情况。

代码语言:javascript
复制
df.info()

共54.5万条数据,没有缺失的变量,且类型除时间time以外都是数值型。

接着看下具体数据,猜测和理解下业务场景,并了解数据的形式。

代码语言:javascript
复制
# 查看前20行    
df.head(20)  

通过以上数据,我们可以发现:

  • 获取方式:这个数据是通过出租车上的机器(比如传感器)采集的GPS信息,并且每隔一段时间进行采集和上报,采集频率不固定
  • 时间数据:每个采集时间都提供了经纬度、载客状态、和车速信息,是一组时间序列数据,但仔细发现原数据时间没有排序。
  • 车辆行驶:经纬度、车速随着车的行驶状态动态变化,比如车停了等红灯车速变为0,经纬度也保持不变,车开起来了车速和经纬度会实时变化。
  • 载客状态根据是否有乘客上车而发生变化,与车辆行驶状态没有任何关系。 出租车的初始状态是0的话,如果有乘客上车,那么载客状态变为1,并且在乘客未下车之前机器采集上报的状态会一直是1,直到乘客下车为止才会再变为0,然后循环反复。

以上是我们对数据的简单理解。

二、数据处理

1)排序

原始数据的时间未进行排序,所以我们无法观察车辆行驶或载客状态的规律,首先需要进行排序。

需求1:对id和time进行升序排序,然后重置索引

代码语言:javascript
复制
# 排序
df = df.sort_values(by = ['id','time']).reset_index(drop=True)
df.head(10)

可以看到time已经按照升序排序了,索引重置为0,1,..,n。

2)类型转换

前面我们发现time变量是object类型,不利于我们做日期的操作,因此我们要转换为时间戳类型。

需求2:将time变量转换为时间戳类型

使用to_datetime方法实现类型转,具体用法可参考传送门。

代码语言:javascript
复制
# 转换日期类型
df['time'] = pd.to_datetime(df['time'])

原数据的time只有时分秒,没有年月日,因此转换后的年月日默认使用了当前日期。

3)重复值

原数据的重复数据较为复杂,常规简单的去重方法无法实现,因此下面通过需求3-7分步骤完成。

需求3:查询id和time重复数据的数量

理论上说,id和time都一样就是重复的数据,因为时间是按一定频率采样的,一个车辆在一个时间点只对应一条数据。因此设置subset子集对id和time查重,同时设置keep=False保留全部重复数据。查重的具体用法可参考。

代码语言:javascript
复制
df_dup = df[df.duplicated(subset=['id','time'], keep=False)==True].reset_index()
df_dup.shape
------
(910, 6)

重复数据全部保留共有910条(这里使用reset_index将原数据df的索引变为变量,后面去重时有用)。

仔细观察发现,重复数据在id和time相同的情况下,其他变量还存在多种不同形式(如下图红框),形式总结如下。

  • status相同都是0或都是1,但经纬度、车速可能不同
  • status不同,是1和0,但经纬度、车速相同

那具体该保留哪个,去除哪个呢?

这需要我们找到一个保留或去除的判断依据。

这里我们尝试通过status的前后变化对重复数据进行判断和筛选。一是因为同一时间不可能有两个载客状态,二是status变化频率低利于观察。

发现了几种不同的形式,我们如何处理呢?

根据status前后变化的规律,处理方式如下:

  • status相同时,但经纬度和车速不同时,删除其一即可,因为采样频率过低无法具体判断哪个是准确的。
  • status不同时,但经纬度和车速相同时,删除时间序列下status异常数据,因为乘客坐车需要时间,载客状态不可能在极短的时间内突然变化。比如,时序下的status为0001000,中间突然出现一个1,那么删除1所在行。同理1110111突然出现一个0,那么删除0所在行(这部分也算是异常值,只不过与重复值交叉同时出现了)。

需求4:对重复数据进行分组的重复数量统计,检查是否有3个以上(包含)重复的

以上重复数据的数量都是2个,那有没有大于2个重复的呢?

数据量太多,肉眼无法观察,我们通过以下语句判断。

代码语言:javascript
复制
(df_dup.groupby(['id','time'])['status'].count()==2).all()
------
False

对id和time分组统计重复数量,是否全部等于2,结果并不是,说明存在大于2个重复的数据。

需求5:筛选出重复数量大于2的数据

既然还存在大于2个重复的数据,那也必须一探究竟,可能形式更复杂。

代码语言:javascript
复制
(
    df_dup.groupby(['id','time'])['status'].count()
          .reset_index()
          .pipe(lambda x:x.loc[x.status>2])
)

id和time分组下有6个重复数量为3的,且最大重复数量就是3了。

对于该情况下status前后的变化情况就需要逐个筛选去看了,这里数量不多大家可以自行观察数据。

经过观察后,我们可以这样做去重的处理:

  • 如果status全部相同,那么任意选一个,比如选第一个
  • 如果status不同,那么基于少数服从多数原则,从多个值里选择一个。比如时序的status值分别为101/110/011,从两个1中选其一;再比如status为001/100/010,从两个0中选其一。

至此,查重部分结束。我们发现了一些规律并且制定了去重的逻辑,那么如何实现去重呢?

需求6:对id和time分组统计status个数、求和,与重复数据df_dup匹配合并

很显然,在这种复杂的情况下直接用drop_duplicates是不管用的,所以我们必须想其他的方法。

下面我们通过加工一组特征来辅助我们进行去重的筛选,对id和time分组统计status个数、求和。

代码语言:javascript
复制
dup_grp = (
    df_dup.groupby(['id','time'])
          .agg(stat_cnt=('status','count'),stat_sum=('status','sum'))
          .reset_index())

加工结果如下,每个唯一的id和time组合都对应着stat_cnt和stat_sum两个特征,根据两个特征值的不同组合就可以判断重复的不同情况了(如图)。

然后我们再通过merge用法将特征值匹配到重复数据df_dup上。

代码语言:javascript
复制
dup_mrg = pd.merge(df_dup, dup_grp, on=['id','time'], how='left')
dup_mrg.head(6)

需求7:根据以上需求3和5中查重的判断逻辑对重复数据筛选,返回需要保留的行索引

这里是去重的最后一步了,前面的处理都是为了这一步的逻辑判断。

可以想到用groupby+apply的方法组合对重复数据分组聚合来进行筛选,结果返回需要保留数据的原数据索引(在需求3中已经重置索引)。

代码语言:javascript
复制
def dup_check(x):
    if (x.stat_cnt.max() == 2) & (x.stat_sum.max() == 0): #重复数量为2,status均为0,返回第一个索引
        return x['index'].values[0]
    elif (x.stat_cnt.max() == 2) & (x.stat_sum.max() == 1):  #重复数量为2,status为10,返回0值的索引
        return x.loc[x.status==0,'index'].values[0]
    elif (x.stat_cnt.max() == 2) & (x.stat_sum.max() == 2): #重复数量为2,status均为1,返回第一个索引
        return x['index'].values[0]
    elif (x.stat_cnt.max() == 3) & (x.stat_sum.max() == 0): #重复数量为3,status均为0,返回第一个索引
        return x['index'].values[0]
    elif (x.stat_cnt.max() == 3) & (x.stat_sum.max() == 1): #重复数量为3,status为100/010/001,返回第一个0值的索引
        return x.loc[x.status==0,'index'].values[0]
    elif (x.stat_cnt.max() == 3) & (x.stat_sum.max() == 2): #重复数量为3,status为110/101/011,返回第一个1值的索引
        return x.loc[x.status==1,'index'].values[0]
    elif (x.stat_cnt.max() == 3) & (x.stat_sum.max() == 3): #重复数量为3,status均为1,返回第一个索引
        return x['index'].values[0]

# 重复数据中需保留的行索引
kp_index = dup_mrg.groupby(['id','time']).apply(dup_check)
# 重复数据中需去掉的行索引
drp_index = dup_mrg.loc[~dup_mrg['index'].isin(kp_index.values),'index']
drp_index

得到保留数据的索引,那么剩下的全都是需要去重的索引了。

最后我们再通过loc筛选从原始数据df中筛选掉这些需要去除的行索引,最终达到去重的目的。

代码语言:javascript
复制
print(df.shape)
df = df.loc[~df.index.isin(drp_index.values)]
print(df.shape)
------
(544999, 6)
(544541, 6)

可以看到共去重了458条数据。

4)异常值

其实前面重复值处理时已经遇到了异常值,但那是在重复情况下发生的异常,一定也还有非重复情况下的异常。

说明:由于是机器采集的GPS数据,在采集过程中可能会因传感器问题出现一定概率的异常值,这是经常发生的,所以我们必须对数据进行异常的排查。

前面提到过,我们发现存在以下异常的情况:同一个车辆在极短的时间内完成载客状态的切换。

比如下面红框里,最大时间差为40秒,载客状态由空客->载客->空客。

理论上说基本不可能,因为乘客乘坐出租车只有40秒的情况极其少见,因此我们判定中间status=1的出现是一种异常情况,需要剔除或将其状态还原为0。

上面是0-1-0的异常,同理1-0-1也是异常,都是短时间内的状态切换。

既然我们发现了这种异常,如何使用pandas将此类异常全部筛选出来呢?

我们给出的判断逻辑是:

  • 载客状态不连续,当前状态与前后状态不一样,比如0-1-0或1-0-1
  • 且这段不连续状态属于同一个车辆id
  • 且这段不连续状态的最大时间差很小,我们设定60秒为阈值

需求8:将id、time、status变量分别上移和下移1个单位,生成6个新变量

现在问题的关键如何用当前状态与前后状态进行对比,pandas中可以使用shift函数对列进行上下的移动,这样就可以实现前后对比了。

代码语言:javascript
复制
# 上下平移一个单位
df['status_up'] = df['status'].shift(1) # 向下移动 1
df['status_down'] = df['status'].shift(-1) # 向上移动 1

df['id_up'] = df['id'].shift(1) # 向下移动 1 
df['id_down'] = df['id'].shift(-1) # 向上移动 1

df['time_up'] = df['time'].shift(1) # 向下移动 1 
df['time_down'] = df['time'].shift(-1) # 向上移动 1

以这样就可以对每一行进行前后值是否相等的判断了。

比如上面图里红框,对status=1status_up=0status_down=0对比以后,筛选出了0-1-0这种异常模式。

需求9:以上存在异常状态的数据全部筛选出来

筛选逻辑如前面所说,以下是对应的5个筛选条件。

代码语言:javascript
复制
#剔除异常数据
cond_1 = (df['status'] != df['status_down'])
cond_2 = (df['status'] != df['status_up'])
cond_3 = (df['id'] == df['id_up'])
cond_4 = (df['id'] == df['id_down'])
cond_5 = ((df['time_down']-df['time_up']).dt.seconds < 60)

df_abn = df[cond_1 & cond_2 & cond_3 & cond_4 & cond_5].reset_index()
df_abn.shape
------
(409, 13)

然后我们就可以得到满足这些条件的异常数据,共有409条(前面去重时还删了一部分)。

代码语言:javascript
复制
# 异常数据id分布
df_abn.id.value_counts()

这409个非重复数据异常值里面,有的车辆(比如id为28159)高达70次的异常。

通过这样的异常检查,我们就可以对车辆数据进行监控,比如每隔一段时间内车辆所发生的异常次数,进而做相应的处置。

需求10:对非重复异常值进行剔除

与重复值去除一样,这里我们通过记录原数据索引的方式,将异常值索引所在行数据从原数据中剔除。

代码语言:javascript
复制
print(df.shape)
df = df.loc[~df.index.isin(df_abn['index'].values)]
print(df.shape)
------
(544541, 12)
(544132, 12)

三、数据分析及可视化

1)订单出行特征

原数据是一个GPS信息表,每一行代表了xx车辆在xx订单下xx时间的GPS数据,是一个明细数据。这非常不利于业务人员使用,业务更多关心的是车辆在什么时间什么地点最终到了哪里去,而不是每时每刻的信息。

需求11:我们需要把GPS信息表转换为出行信息表

转换后的形式如上图所示,地点可用经纬度代替。

那么这个转换过程如何实现呢?

可以通过下面两个步骤实现。

  • 捕捉每个订单上下车的时间和地点,并筛选出来

判断条件是:如果此时点的status载客状态与上一状态差为1,即由0变为1,说明是上车。反之,如果由1变为0则差值为-1,即为下车。

那么用此时点与上一时点状态作差还是可以通过shift偏移来实现,前面检查异常值时我们已经创建了辅助特征status_up和id_up,所以这里直接拿来用即可。

将状态差值为1(上车)和 -1(下车)筛选出来,并且两个状态下需为同一辆车。

代码语言:javascript
复制
# 与上一个状态作差
df['status_chg'] = df['status']-df['status_up']
# 车辆id与上一个作差
df['id_chg'] = df['id']-df['id_up']
# 筛选上车和下车的标识1和-1,并为同一车辆
df_temp = df.loc[((df['status_chg']==1) | (df['status_chg']==-1)) & (df['id_chg']==0)]
  • 将上、下车时间的地点时间错位拼接

现在上下车分别为两行数据,我们最终想移动变成一行。还是利用shift将我们想要的变量向上偏移一个单位即可。偏移后每一行都是上车、下车或下车、上车的信息,我们最后再通过loc筛选从上车到下车的所有行,同样指定是同一车辆。

最后修改列名完成了出行信息表。

代码语言:javascript
复制
# 将时间、经纬度向上偏移1个单位
df_temp['Etime'] = df_temp['time'].shift(-1)
df_temp['Elong'] = df_temp['long'].shift(-1)
df_temp['Elati'] = df_temp['lati'].shift(-1)
# 
df_order = df_temp.loc[(df_temp['status_chg']==1) & (df_temp['id']==df_temp['id'].shift(-1)),
                       ['id','time','long','lati','Etime','Elong','Elati']]
# 修改列名
df_order.columns=['车辆id','开始时间','开始经度','开始纬度','结束时间','结束经度','结束纬度']
df_order.head(10)

下面根据已完成的订单出行信息表进行一些统计分析。

2)订单时段数量统计

需求12:统计各小时的订单数分布

前面我们已经将time时间转换为时间类型了,那么将时间戳转换为小时就非常简单了,时间属性方法可以参考传送门。转换后为一天0到24小时之内的小时数值,比如2023-06-28 04:30:13转换为小时4。

然后对小时groupby分组求订单数量即可,最后使用pandas的内置方法进行可视化,可视化方法参考传送门。

代码语言:javascript
复制
# 转换为小时
df_order['小时'] = df_order['开始时间'].dt.hour
# 对小时分组求订单数量
df_hourcnt = df_order.groupby('小时')['车辆id'].count()      
df_hourcnt = df_hourcnt.rename('数量').reset_index()   

# 绘制折线图    
fig = plt.figure(1,(8,4),dpi = 200)    
ax = plt.subplot(111)    
plt.plot(df_hourcnt['小时'],df_hourcnt['数量'],'k-')    
plt.plot(df_hourcnt['小时'],df_hourcnt['数量'],'k.')    
plt.bar(df_hourcnt['小时'],df_hourcnt['数量'])    
plt.ylabel('数量')  
plt.xlabel('小时')  
plt.xticks(range(24),range(24))  
plt.title('出行小时数量统计')    
plt.ylim(0,350)    
plt.show()

观察结果:

  • 出租车在各时段都有订单出行,其中早上8点到晚上5点起伏较为平稳,从晚上8点到10点是高峰期,后半夜3点到5点(夜车)是低峰期。观察结果比较符合我们的现实情况。
3)订单时长分布

需求13:统计各时段订单的时长分布

下面通过箱型图对各时段订单时长做可视化。

代码语言:javascript
复制
# 开始结束时间作差并转化为秒单位
df_order['订单时长'] = (df_order['结束时间']-df_order['开始时间']).dt.seconds
# 各时段订单时长箱型图 
fig = plt.figure(1,(6,4),dpi = 150)      
ax = plt.subplot(111)  
plt.sca(ax)  
sns.boxplot(x="小时", y=df_order["订单时长"]/60, data=df_order,ax=ax)  
plt.ylabel('订单时长(分钟)')  
plt.xlabel('订单时段')  
plt.ylim(0,60)  
plt.show()  

观察结果:

  • 订单时长在各时段均有一定的差异,但在早晚高峰时段(8-9点、17-18点)订单时长明显多,是因为高峰期堵车所以导致时间比较长。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-08-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python数据科学 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、数据处理
  • 二、数据处理
  • 三、数据分析及可视化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档