大家好,我是“蒋点数分”,多年以来一直从事数据分析工作。从今天开始,与大家持续分享关于数据分析的学习内容。

本文是第一篇,也是【SQL 周周练】系列的第一篇。该系列是挑选或自编具有一些难度的 SQL 题目,一周至少更新一篇。后续创作的内容,初步规划的方向包括:

后续内容规划

1.利用 Streamlit 实现 Hive 元数据展示SQL 编辑器、 结合Docker 沙箱实现数据分析 Agent
2.时间序列异常识别、异动归因算法
3.留存率拟合、预测、建模
4.学习 AB 实验、复杂实验设计等
5.自动化机器学习、自动化特征工程
6.因果推断学习
7. .……

欢迎关注,一起学习。

第 1 期题目

题目来源:Uber 面试真题

一、题目介绍

有一张表,记录了乘客对于司机的评价,请找出每个星期当中连续获得 5 星好评最多的 driver_id。列名:driver_idrating_timeratings (原题乘客 id 对解答题目是冗余的,故此我在文中省略掉...)连续 5 星,中间出现任意一次非 5 星,则中断。

二、题目思路

想要答题的同学,可以先思考答案。
……

……

……

我来谈谈我的思路,“连续”问题是数据分析师在 SQL 笔试中的“老朋友”了。最常见的就是“连续登录”问题,其大概思路是利用日期减去排序row_number()得到一个“基准日期”用来作为分组标识。这里没有日期,不能生搬硬套。

我们思维变通一下,如果想将连续计数的记录能够放在同一个组里,那么这个分组标识是关键。对于连续 5 星,它们的有什么共同点?是每一个 5 星评价前面有多少个非 5 星(1~4 星)的评价。为了方便理解,我绘制一个简易的说明图:

只需要注意剔除每组开头可能多出来的非 5 星评价,即可完成统计。下面,我用 NumPy 结合一些假设来生成模拟的数据集:

三、生成模拟数据

只关心 SQL 代码的同学,可以跳转到第四节(我在工作中使用 Hive 较多,因此采用 Hive 的语法)

为了简化模拟数据的难度,做如下假设:

1.假设用户下车之后立即评价,评价时间取下车时间
2.司机等待订单、接客送客加在一起的时间间隔,通过指数分布模拟
3.订单的时间间隔,不引入早晚高峰因素,不引入差异化因素 => 对每名司机的参数是一样的
4.司机回家和睡觉的时间,算在一起,用正态分布模拟
5.不引入司机吃饭、出车前休息等个人事务的时间,否则模拟起来太复杂
6.对于司机,只限制每日最多在线时长,不做周、月级别的限制
7.假设存在两类司机:
a.追求每天达到一个目标收入,达到后则主动收车 => 用单量代替收入
b.追求每天达到某个在线时长,达到后则主动收车
8.模拟数据累计后,可能导致的司机日夜规律颠倒 => 违背现实情况,不作调整

模拟代码如下:

1. 定义模拟逻辑需要的常量
import datetime
import numpy as np
import pandas as pd # 设置随机数种子
np.random.seed(2025)
# 模拟的司机数量
DRIVER_NUM = 100
# 追求单量的司机数量(不论追求单量还是追求在线时长,都要额外受平台在线时长限制)
PURSUING_ORDER_DRIVER_NUM = 55
# 追求订单的数量取值 (10 ~ 20 单,值太高在其他参数影响下,也取不到)
# 离散均匀分布
pursuing_order_volume = np.random.choice(
np.arange(10, 21), size=PURSUING_ORDER_DRIVER_NUM
)
# 追求在线时长的司机数量
PURSUING_ONLINE_DRIVER_NUM = DRIVER_NUM - PURSUING_ORDER_DRIVER_NUM
# 追求在线时长的取值 (8小时、8.5小时......12小时)
pursuing_online_duration = np.random.choice(
np.arange(8, 12.5, 0.5), size=PURSUING_ONLINE_DRIVER_NUM
)
# 模拟数据的日期范围
START_DATETIME = datetime.datetime(2025, 1, 1, 8, 0, 0)
END_DATETIME = datetime.datetime(2025, 5, 1, 23, 59, 59)
# 平均订单时间间隔(单位秒,包含等单+接客+送客,等于评价时间间隔)
ORDER_INTERVAL_AVG = 40 * 60
# 司机平均休息时长(单位秒,包含收车时间)
DRIVER_REST_DURATION_AVG = 8 * 3600
# 司机平均休息时长标准差(单位秒)
DRIVER_REST_DURATION_STD = 30 * 60
# 每日在线时长上限(秒)
ONLINE_DURATION_UPPER_LIMIT = 12 * 3600
2. 模拟订单间隔、乘客评分、休息间隔。为了提高生成速度,尽量一次让 NumPy 生成足够多的数据;用函数封装起来,如果超出了预先生成的数据长度,则开启单次生成:
# 为了一次尽可能将数据模拟全
# 根据参数平均值,来计算出大概需要模拟出多少个订单间隔,再增加 10% 浮动
# round 函数输出 float 类型,需要转为 int 类型,不然后续 numpy 的 size 会报错
ORDER_NUM_NEED_SIMULATION = int(
round(
(END_DATETIME - START_DATETIME).days
* (ONLINE_DURATION_UPPER_LIMIT / ORDER_INTERVAL_AVG)
* (1 + 0.1),
0,
)
) # 生成模拟的订单间隔
order_interval_simulation = np.random.exponential(
scale=ORDER_INTERVAL_AVG, size=(DRIVER_NUM, ORDER_NUM_NEED_SIMULATION)
) # 乘客的评价也一并随机生成
rating_simulation = np.random.choice(
np.arange(1, 6),
size=(DRIVER_NUM, ORDER_NUM_NEED_SIMULATION),
p=[0.01, 0.01, 0.02, 0.06, 0.9],
) defget_order_interval_and_rating_simulation(driver_id, cnt):
"""
获取订单间隔时长和订单评分,增加一个函数,
是为了如果批量随机生成的数据不够用,再单次生成
"""
if cnt >= ORDER_NUM_NEED_SIMULATION:
return (
np.random.exponential(scale=ORDER_INTERVAL_AVG),
np.random.choice(np.arange(1, 6), p=[0.01, 0.01, 0.02, 0.06, 0.9]),
)
else:
return (
order_interval_simulation[driver_id][cnt],
rating_simulation[driver_id][cnt],
) # 模拟休息的数据( 在线加休息的和有可能小于 24 小时 )
REST_NUM_NEED_SIMULATION = int(
round((END_DATETIME - START_DATETIME).days * (1 + 0.1), 0)
) rest_interval_simulation = (
np.clip(
np.random.normal(loc=8, scale=0.5, size=(DRIVER_NUM, REST_NUM_NEED_SIMULATION)),
a_min=6,
a_max=12,
)
* 3600
) defget_rest_interval_simulation(driver_id, cnt):
"""
获取休息间隔时长,增加一个函数,是为了如果批量随机生成的
数据不够用,再单次生成
"""
if cnt >= REST_NUM_NEED_SIMULATION:
return np.clip(np.random.normal(loc=8, scale=0.5), a_min=6, a_max=12) * 3600
else:
return rest_interval_simulation[driver_id][cnt]
3. 根据假设的逻辑,生成司机的全部数据。注意司机休息的判断条件,以及中间变量清零的处理:
table_data = {"driver_id": [], "rating_time": [], "ratings": []}

for driver_id inrange(DRIVER_NUM):
order_cnt = 0# 第几个订单
rest_cnt = 0# 第几次休息
last_time = START_DATETIME
# 当前累计在线时间,注意单位是秒
current_online_time = 0
# 当天的订单,追求订单的司机需要这个变量
current_order_cnt = 0
whileTrue:
table_data["driver_id"].append(driver_id)
order_interval, rating = get_order_interval_and_rating_simulation(
driver_id, order_cnt
)
last_time = last_time + datetime.timedelta(seconds=int(order_interval))
table_data["rating_time"].append(last_time)
table_data["ratings"].append(rating) # 当天累计在线时间增加
current_online_time += order_interval
# 订单序号加一
order_cnt += 1
# 当天订单数量加一
current_order_cnt += 1 # 当天累计时间超过平台限制,需要去休息
rest_flag_1 = current_online_time >= ONLINE_DURATION_UPPER_LIMIT
# 前面的司机追求订单数
rest_flag_2 = (
driver_id < PURSUING_ORDER_DRIVER_NUM
and current_order_cnt >= pursuing_order_volume[driver_id]
)
# 后面的司机追求在线时长
rest_flag_3 = (
driver_id >= PURSUING_ORDER_DRIVER_NUM
and current_online_time
>= pursuing_online_duration[driver_id - PURSUING_ORDER_DRIVER_NUM]
) if rest_flag_1 or rest_flag_2 or rest_flag_3:
# 增加休息时间
reset_interval = int(get_rest_interval_simulation(driver_id, rest_cnt))
last_time = last_time + datetime.timedelta(seconds=reset_interval)
# 当天累计在线时长清零
current_online_time = 0
# 当天累计订单数清零
current_order_cnt = 0
# 休息次数加一
rest_cnt += 1 # 达到项目总体模拟结束时间,跳出
if last_time > END_DATETIME:
break
4. 将模拟的数据转为 pd.DataFrame 并输出为 csv 文件;创建 Hive 表,并将数据 load 到表中:
df = pd.DataFrame(table_data)
df["driver_id"] = "driver_" + df["driver_id"].astype("str").str.zfill(2)
df.to_csv(
"./dwd_uber_simulation_rating_detail.csv",
sep=",",
encoding="utf-8-sig",
index=False,
header=False,
) from pyhive import hive # 配置连接参数
host_ip = "127.0.0.1"
port = 10000
username = "蒋点数分" with hive.Connection(host=host_ip, port=port) as conn:
cursor = conn.cursor() create_table_sql = """
CREATE TABLE IF NOT EXISTS data_exercise.dwd_uber_simulation_rating_detail (
driver_id STRING COMMENT '司机id',
rating_time TIMESTAMP COMMENT '评价时间',
ratings TINYINT COMMENT '评分等级,1~5 星'
)
COMMENT 'Uber 乘客对司机评分表,模拟数据 | 文章编号 7c98d8ef'
ROW FORMAT DELIMITED
FIELDS TERMINATED BY ','
STORED AS TEXTFILE
""" cursor.execute(create_table_sql) import os load_data_sql = f"""
LOAD DATA LOCAL INPATH "{os.getcwd() + '/dwd_uber_simulation_rating_detail.csv'}"
OVERWRITE INTO TABLE data_exercise.dwd_uber_simulation_rating_detail
""" cursor.execute(load_data_sql)
5. 将查询的 SQL,利用 pd.read_sql_query 读取查询结果。注意此段代码,仍然位于 with 上下文中:
    select_data_sql = '''
with calc_table as (
select
driver_id, date_format(rating_time, 'yyyy年ww周') as year_week -- 从周日开始算新的一周
, sum(if(ratings <> 5, 1, 0)) over(partition by driver_id, date_format(rating_time, 'yyyy年ww周') order by rating_time asc) as cnt_tag
, ratings
from data_exercise.dwd_uber_simulation_rating_detail
) , calc_continuous_five_table as (
select
driver_id, year_week, cnt_tag
, sum(1) as continuous_five -- sum(if(raings=5,1,0))
, rank() over(partition by year_week order by sum(1) desc) as rk
from calc_table
where ratings = 5
group by driver_id, year_week, cnt_tag
) select
year_week
-- 可能有司机并列,使用 collect
-- 如果一名司机连续 5 星的次数最高,且出现了两次,那么会重复
-- 因此使用 set
, collect_set(driver_id) as most_continuous_five_start_drivers
from calc_continuous_five_table
where rk = 1
group by year_week
''' df_outcome = pd.read_sql_query(select_data_sql, conn) # 在 Jupter 环境下,显示结果
display(df_outcome)

我使用 PyHive 包实现 Python 操作 Hive。我个人电脑部署了 Hadoop 及 Hive,但是没有开启认证,企业里一般常用 Kerberos 来进行大数据集群的认证。

四、SQL 解答

我采用 CTE 的写法来嵌套逻辑转为串行,这样写对于复杂 SQL 的逻辑梳理具有一定帮助。使用窗口函数 count(if(rating<>5,rating,null)) 或 sum(if(rating<>5,1,0)) 来统计 1~4 星评价的数量。

“每周”因此需要使用 date_format 来提取年份和周 => partition by driver_id, date_format(rating_time, 'yyyy年ww周');使用 order by rating_time asc 时,统计的窗口范围默认是 rows between preceding unbounded and current row,写清楚更好。

注意因为统计的逻辑是截至当前行,所以第一个 5 星评价前的那个 1~4 星,它的计数标识跟 5 星是一样的。所以需要 where 过滤,当然也可以在后续聚合统计时,使用条件处理 sum(if(raings=5,1,0))

最终结果使用 collect_set 将 driver_id 形成去重数组:一方面可能每个星期有司机连续 5 星好评数并列第一;另一方面极端情况下,连续 5 星好评最多的那个司机如果最多的连续 5 星好评数一周内出现了多次,则这个 driver_id 会出现多次,这是为什么不用 collect_list 的原因。

with calc_table as (
select
driver_id, date_format(rating_time, 'yyyy年ww周') as year_week -- 从周日开始算新的一周
, sum(if(ratings <>5, 1, 0)) over(partitionby driver_id, date_format(rating_time, 'yyyy年ww周') orderby rating_time asc) as cnt_tag
, ratings
from data_exercise.dwd_uber_simulation_rating_detail
) , calc_continuous_five_table as (
select
driver_id, year_week, cnt_tag
, sum(1) as continuous_five -- sum(if(raings=5,1,0))
, rank() over(partitionby year_week orderbysum(1) desc) as rk
from calc_table
where ratings =5
groupby driver_id, year_week, cnt_tag
) select
year_week
-- 可能有司机并列,使用 collect
-- 如果一名司机连续 5 星的次数最高,且出现了两次,那么会重复
-- 因此使用 set
, collect_set(driver_id) as most_continuous_five_start_drivers
, max(continuous_five) as continuous_five
from calc_continuous_five_table
where rk =1
groupby year_week

需要注意的是,date_format 的 w 参数是从周日开始算新的一周。我这里偷懒就不改成按照周一为新的一周来计算。

最简单的思路是将实际日期往前挪一天,但是周数与跨年问题,往往容易引起混淆,实际使用时需要小心处理。严谨起见,应查询 ISO 8601 的规定。

我现在正在求职数据类工作(主要是数据分析或数据科学);如果您有合适的机会,恳请您与我联系,即时到岗,不限城市。您可以发送私信或联系我(全网同名:蒋点数分)。

【Uber 面试真题】SQL :每个星期连续5星评价最多的司机的更多相关文章

  1. 面试系列二:精选大数据面试真题JVM专项-附答案详细解析

    公众号(五分钟学大数据)已推出大数据面试系列文章-五分钟小面试,此系列文章将会深入研究各大厂笔面试真题,并根据笔面试题扩展相关的知识点,助力大家都能够成功入职大厂! 大数据笔面试系列文章分为两种类型: ...

  2. WEB前端面试真题 - 2000!大数的阶乘如何计算?

    HTML5学堂-码匠:求某个数字的阶乘,很难吗?看上去这道题异常简单,却不曾想里面暗藏杀机,让不少前端面试的英雄好汉折戟沉沙. 面试真题题目 如何求"大数"的阶乘(如1000的阶乘 ...

  3. 分享13道上海尚学堂拿回来的Java面试真题,这些都是Java核心常见问题,想拿OFFER必看!

    上海尚学堂Java培训学员参加面试带回来的真题,分享出来与大家,希望大家能认真地看看做一遍.后面有详细题解答案,对照下,看看自己做得怎么样,把这些面试遇到的真题全部掌握,做好面试笔试前的准备. 一.1 ...

  4. Python面试真题答案或案例

    Python面试真题答案或案例如下: 请等待. #coding=utf-8 #1.一行代码实现1--100之和 print(sum(range(1,101))) #2.如何在一个函数内部修改全局变量 ...

  5. 2018最新大厂Android面试真题

    前言 又到了金三银四的面试季,自己也不得不参与到这场战役中来,其实是从去年底就开始看,android的好机会确实不太多,但也还好,3年+的android开发经历还是有一些面试机会的,不过确实不像几年前 ...

  6. 拼多多后台开发面试真题:如何用Redis统计独立用户访问量

    众所周至,拼多多的待遇也是高的可怕,在挖人方面也是不遗余力,对于一些工作3年的开发,稍微优秀一点的,都给到30K的Offer,当然,拼多多加班也是出名的,一周上6天班是常态,每天工作时间基本都是超过1 ...

  7. 拼多多面试真题:如何用 Redis 统计独立用户访问量!

    阅读本文大概需要 2.8 分钟. 作者:沙茶敏碎碎念 众所周至,拼多多的待遇也是高的可怕,在挖人方面也是不遗余力,对于一些工作 3 年的开发,稍微优秀一点的,都给到 30K 的 Offer. 当然,拼 ...

  8. 大厂0距离:网易 Linux 运维工程师面试真题,内含答案

    作为 Linux 运维工程师,进入大公司是开启职业新起点的关键,今天马哥 linux 运维及云计算智囊团的小伙伴特别分享了其在网易面试 Linux 运维及云计算工程师的题目和经历,希望对广大 Linu ...

  9. 再也不用担心问RecycleView了——面试真题详解

    关于RecycleView,之前我写过一篇比较基础的文章,主要说的是缓存和优化等问题.但是有读者反映问题不够实际和深入.于是,我又去淘了一些关于RecycleView的面试真题,大家一起看看吧,这次的 ...

  10. 【Java面试真题】剑指Offer53.2——0~n-1中缺失的数字(异或、二分两种解法)

    [Java实现]剑指Offer53.2--0~n-1中缺失的数字:面试真题,两种思路分享 前面有另一道面试题[Java实现]剑指offer53.1--在排序数组中查找数字(LeetCode34:在排序 ...

随机推荐

  1. Linux嵌入式设备怎么确定网络端口的速率

    Linux嵌入式设备怎么确定网络端口的速率 突发奇想,就是Linux下面我能不能查询到端口的速率,以此来判断要不要频繁的发送网络数据包呢? 或者更换包利用率更高的协议呢. 于是抱着这样的想法,我开始学 ...

  2. 论今日,Vue VSCode Snippets 不进行代码提示的问题 或 vetur Request textDocument/documentSymbol failed.

    这他喵的是因为 vetur 这个鬼东西升级了,然后和项目中某些包不匹配了, 降级就好了, 法克尤啊法克尤,我整了一天,大概是坏了吧 灵感来源:https://cxymm.net/article/a84 ...

  3. glib-2.60在win64,msys2下编译

    前阵子,工作原因,需要在win7 64下的msys2来编译glib,下面是一些踩过的坑: 事先声明一下,这些个解决方式及纯粹是为了编译通过,可能有些做法不太适合一些需要正常使用的场合,烦请各位注意下. ...

  4. minio迁移工具 mc

    mc mirror 命令属于 MinIO Client (mc) 工具,默认不会随 MinIO 服务器一起安装,需要 单独安装. 安装 MinIO Client (mc) Linux/macOS 执行 ...

  5. linux的zip命令详解 | Linux文件打包成Zip的命令和方法

    zip 命令用来压缩文件 参数: -A:调整可执行的自动解压缩文件: -b<工作目录>:指定暂时存放文件的目录: -c:替每个被压缩的文件加上注释: -d:从压缩文件内删除指定的文件: - ...

  6. Flink 实战之流式数据去重

    系列文章 Flink 实战之 Real-Time DateHistogram Flink 实战之从 Kafka 到 ES Flink 实战之维表关联 Flink 实战之流式数据去重 流式数据是一种源源 ...

  7. Linux下使用fdisk扩大分区容量

    磁盘容量有300GB,之前分区的时候只分了一个150GB的/data分区,现在/data分区已经不够用了. 需求:把这块磁盘剩余的150GB容量增加到之前的/data分区,并且保证/data分区原有的 ...

  8. Oracle10g RAC -- Linux 集群文件系统

    通常,集群只是一组作为单一系统运行的服务器( PC 或者工作站).但是,这个定义的外延不断显著扩大:集群技术现在不但是一个动态领域,而且其各种应用程序正不断吸收新的特性.此外,集群文件系统技术(无论是 ...

  9. elementui|dropdown|下拉菜单作为模态框使用

    elementui|dropdown|下拉菜单作为模态框使用 背景 场景:下拉菜单作为模态框使用: 操作:下拉菜单设置触发条件点击展示/隐藏:trigger="click" 目的: ...

  10. 网络编程-Netty-writeAndFlush方法原理分析 以及 close以后是否还能写入数据?

    前言 在上一讲网络编程-关闭连接(2)-Java的NIO在关闭socket时,究竟用了哪个系统调用函数?中,我们做了个实验,研究了java nio的close函数究竟调用了哪个系统调用,答案是clos ...