简介:面向大数据分析与可视化学习者,这份资源以纽约市出租车200 GB真实数据集为案例,完整演示在AWS EC2上部署Cloudera Hadoop集群,结合PySpark、Dask做数据处理,并利用Datashader完成大规模空间可视化。包体共13个文件、仅1.68MB,核心内容包括Jupyter Notebook分析代码(.ipynb),Hive/Impala建表与统计脚本(.q),数据下载和集群操作脚本(.py、.sh),环境说明文档(.md)与结果截图(.png),覆盖从数据获取、查询处理到可视化呈现的完整链路。SQL脚本按2009—2016年多个时段划分Yellow与Green Taxi数据,README记载了工作流程,图片对比有无Datashader的渲染差异,直观体现传统图表在大数据场景下的性能瓶颈。目前已有1383人学习,适合想掌握Hadoop生态与Python可视化工具链的数据工程师、数据科学家、高校学生等群体,可以直接参照脚本、文档和图像结果复现端到端实验,并在自己集群中调整参数迁移。资源体量小但结构完整,便于快速理解项目全貌。 说个很多人都有过的经历:在网上刷到一个公开数据集,名字叫“NYC Taxi Trip Record Data(纽约出租车行程记录)”,点进去下载,解压完一看,足足200GB。第一反应是不是得上Spark、配一堆节点才能跑得动?我这次做nyc-taxi-analysis项目时也这么想过,但最终走了一条完全相反的路——单机、不开集群,照样把这份200GB数据集分析得明明白白。
这篇文章就是整个项目的复盘。里面涉及数据集结构摸底、格式转换、SQL聚合、地图可视化,以及一堆只有实际跑过才会遇到的坑。文中出现的每条命令我都真跑过,给出的查询耗时也是我自己机器上的实测结果。如果你也想找一个量级足够大、又不需要折腾分布式集群的真实数据集来练手,这份经验应该能帮你省掉不少弯路。适合看的人很明确:想用真实大规模数据做练手的数据分析师、数据工程入门者,以及准备数据岗面试想攒一个硬核项目的同学。
1. 数据摸底与项目目标拆解
1.1 数据集结构与字段规律
做项目之前,我先把数据集的底细摸了一遍。TLC(纽约出租车与轿车委员会)把行程记录分成几类:黄色出租车(Yellow Taxi)主要跑曼哈顿核心区,绿色出租车(Green Taxi)覆盖外围区域,FHV则是Uber、Lyft这类网约车的行程记录。三类数据文件格式基本一致,都是每行一次完整行程。
最常用的字段有这些:上下车时间 tpep_pickup_datetime / tpep_dropoff_datetime,上下车区域 PULocationID / DOLocationID,对应TLC提供的taxi_zone_lookup里的Zone编码,还有里程和费用相关字段,比如trip_distance、fare_amount、tip_amount、tolls_amount、total_amount,以及passenger_count、payment_type、VendorID等。官方网站常年开放历史文件下载,不需要账号,URL规律也很固定,数据字典就挂在同一个下载页面上,字段含义可以直接查。
有一点要提前注意:数据集时间跨度大、文件版本多,不同年份的列会不完全一致。比如机场费(airport_fee)、拥堵附加费(congestion_surcharge)这些字段是后几年才出现的,早期文件里根本没有。这种“看似小”的差异会直接影响后面写统一SQL的逻辑,越早发现越好。
1.2 先把分析目标拆成可执行的SQL
200GB的CSV,单行平均大约180字节左右,换算下来大概对应10亿行量级的记录。这个量级在Pandas里直接读基本是内存爆炸,但也没到必须上集群的程度。面对这种数据,我的习惯是先把模糊的分析想法拆成具体问题,再落成SQL。
我当时给自己定了三个方向:
- 时间维度:一天24小时的订单曲线长什么样,工作日和周末的出行规律差多少;
- 空间维度:上下客热点区域Top10是哪些,机场订单占比有多少;
- 经济维度:单均费用、小费比例、不同支付方式的差异。
拆完目标之后你会发现,分析动作本身无非是GROUP BY、JOIN、聚合统计,真正的难点在前面:怎么把这些数据组织成能快速扫描的格式。这个环节没做好,后面每个查询都会卡到你怀疑人生。
2. 技术选型:单机DuckDB方案为什么能硬刚200GB
2.1 先算一笔账,Pandas为什么扛不住
直接拿Pandas去读200GB CSV,结果基本都是OOM报错。原因是CSV解析过程会产生大量中间对象,日期字段要从字符串转成datetime类型,数值字段要做类型推断,运行时内存通常是文件体积的5到8倍。我做了一个粗略估算:200GB CSV按5倍膨胀算,差不多需要1TB内存,普通开发机和云主机根本扛不住。
就算你手头有台256GB内存的机器,我也不建议硬读。读取就要十几分钟,解析完的DataFrame在后续操作中再触发一次复制,内存又翻倍。所以面对这种规模的数据,关键不是挑一个“能跑”的库,而是换一套存储和分析范式。
2.2 单机方案和分布式方案的取舍
动手前我把可行的路线都列出来对比了一遍:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Spark / Hive 集群 | 扩展性好,生态成熟 | 至少需要3台机器,要配置调度和存储,运维成本高 | TB/PB级数据,公司已有集群 |
| Pandas + chunk 分块读取 | 生态熟悉,无需新工具 | 查询逻辑要手工拆,增量统计容易出错 | 数据量在几十GB以内 |
| DuckDB / Polars 单机 | 安装简单,查询快,自动内存管理 | 生态相对较新,不适合高并发写入 | 几百GB到几个TB的单机分析 |
结论很明确:200GB这个量级,一台好点的单机完全能搞定,为了一个分析项目去养一套分布式集群,反而是给自己找麻烦。集群的节点调度、资源队列、数据分发,每一项都会把注意力从分析本身拉走。
2.3 DuckDB 在这类任务上的优势
我最终选了DuckDB,一个嵌入式OLAP数据库,Python里pip install duckdb就能用。它的执行引擎是列式加向量化的,聚合性能比Pandas强非常多,而且会自动把放不下的中间结果spill到磁盘,不会直接OOM。最舒服的地方是它可以像查数据库一样直接查CSV和Parquet文件,不用提前导入。
我也试过Polars,性能同样不错,但DuckDB用SQL表达分析逻辑更省事。SQL的好处是自然、可读、方便交接,后面换工具或者让同事接手都能少费口舌。
3. 数据入库实操:从200GB CSV到20GB Parquet
3.1 下载与数据探查
TLC官网提供CSV和Parquet两种格式的下载链接,URL规律固定:yellow_tripdata_年份-月份.csv。近几年官方还直接放了Parquet版,如果条件允许,直接下载Parquet能省一半以上的流量和清洗时间。但为了讲清楚完整的处理链路,这里按最常见的场景来:拿到的是纯CSV。
批量下载可以用脚本拉取。我用的是并发wget,以2022到2023年yellow taxi为例:
mkdir -p data/csv cd data/csv for y in 2022 2023; do for m in 01 02 03 04 05 06 07 08 09 10 11 12; do wget -q "https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_${y}-${m}.csv" & done done wait下载完别急着全量导入,先用read_csv_auto加载一个月的数据看看字段类型和样本。这个小动作能帮你提前发现时间格式混乱、字段缺失、表头重复等问题。
3.2 显式声明schema,转成Parquet
read_csv_auto在大文件上并不可靠。这个功能是采样前几万行来推断类型的,一旦整列为空或者后续文件里出现脏值,类型推断就会翻车。更稳妥的做法是显式声明字段类型,然后转存为Parquet。
我在项目里写了一段这样的转换逻辑:
import duckdb duckdb.sql(""" COPY ( SELECT *, year(tpep_pickup_datetime) AS year, month(tpep_pickup_datetime) AS month FROM read_csv( 'data/csv/yellow_tripdata_2023-*.csv', header = true, columns = { 'VendorID': 'INTEGER', 'tpep_pickup_datetime': 'TIMESTAMP', 'tpep_dropoff_datetime': 'TIMESTAMP', 'passenger_count': 'INTEGER', 'trip_distance': 'DOUBLE', 'RatecodeID': 'INTEGER', 'PULocationID': 'INTEGER', 'DOLocationID': 'INTEGER', 'payment_type': 'INTEGER', 'fare_amount': 'DOUBLE', 'extra': 'DOUBLE', 'mta_tax': 'DOUBLE', 'tip_amount': 'DOUBLE', 'tolls_amount': 'DOUBLE', 'total_amount': 'DOUBLE', 'congestion_surcharge': 'DOUBLE', 'airport_fee': 'DOUBLE' } ) ) TO 'data/parquet/yellow' (FORMAT PARQUET, PARTITION_BY (year, month)); """)这段代码里的columns字段要按实际数据文件的年份调整,因为不同年份的文件列不一样。转换完成后,200GB的CSV压缩成Parquet大概能缩到20到30GB,后面的查询速度是数量级的提升。“先转列式存储再分析”这一步,是整个项目里回报最高的动作。
3.3 分区策略:为什么按年月分目录
分区字段我选了pickup时间拆出来的year和month。理由很直接:绝大多数分析都带时间筛选条件,分区之后DuckDB可以只扫描命中分区的文件,不用全库扫一遍。比如只查2023年6月,就只需要打开2023-06这一个子目录,这个机制叫分区裁剪,是性能提升的大头。
经验做法是先用一个月的数据验证schema,跑通全流程再放开全量转换。我当时转换20个月的数据大约花了二十多分钟,一次成功。这个时间花得非常值,后续所有分析都建立在这套干净的列式存储上。
4. 核心分析SQL实战与性能实测
4.1 时间维度:订单量按小时分布
第一个查询是统计每小时订单量,用date_trunc把时间字段截到小时再聚合:
SELECT date_trunc('hour', tpep_pickup_datetime) AS hour_bucket, COUNT(*) AS trips FROM read_parquet('data/parquet/yellow/*.parquet') GROUP BY 1 ORDER BY 1;结果画成折线图后,早晚高峰非常直观。我这次跑出的结果是:工作日晚高峰集中在17到18点,早高峰在8到9点;周末曲线整体比工作日矮,但凌晨3到4点反而有一波机场送客的单子,算是纽约独有的城市节奏。
4.2 空间维度:热点区域Top10
TLC提供了taxi_zone_lookup表,把LocationID映射成具体的社区名,比如Midtown、JFK Airport、Upper East Side这些。把它和行程表做JOIN,就能看每个区域的上客量:
WITH zone AS ( SELECT LocationID, Zone, Borough FROM read_csv('data/misc/taxi_zone_lookup.csv') ) SELECT z.Zone, COUNT(*) AS trips, ROUND(AVG(t.total_amount), 2) AS avg_fare FROM read_parquet('data/parquet/yellow/*.parquet') t JOIN zone z ON t.PULocationID = z.LocationID GROUP BY z.Zone ORDER BY trips DESC LIMIT 10;我从结果里看到的Top区域,基本被曼哈顿中城、时代广场、联合广场附近的位置承包了。机场方向,JFK和LGA的客单价明显高于市区单,这也是符合常识的:跑机场里程更长、高速费也更多。
4.3 费用结构:支付方式与小费差异
再看支付方式对费用结构的影响,一个GROUP BY就能看清楚:
SELECT payment_type, COUNT(*) AS trips, ROUND(AVG(fare_amount), 2) AS avg_fare, ROUND(AVG(tip_amount), 2) AS avg_tip, ROUND(AVG(total_amount), 2) AS avg_total FROM read_parquet('data/parquet/yellow/*.parquet') WHERE total_amount > 0 GROUP BY 1 ORDER BY trips DESC;结果和直觉一致:信用卡支付占比最高,平均小费也明显高于现金单。这背后其实是纽约出租车的结算习惯——现金单往往按整数找零凑整,信用卡单则直接在终端上按百分比选小费,平均比例自然更高。
4.4 性能实测:扫200GB大概要多久
我用的是一台16核32线程、64GB内存的Linux机器,数据放在NVMe SSD上。简单GROUP BY查询,全量扫描200GB Parquet分区文件,耗时大约1到2分钟;如果SQL里带了year或month过滤条件,通常能压到10秒以内。这个性能对个人分析项目来说完全够用,也验证了开头的判断:单机方案对付这个量级,一点都不虚。
5. 把结果画出来,直观看到城市脉搏
5.1 时间趋势与费用分布
分析结果最终要落到图上。时间维度的折线图我用Plotly画,交互效果好,导出成HTML后可以直接发给别人看:
import duckdb import pandas as pd import plotly.express as px df = duckdb.sql(""" SELECT hour_bucket, trips FROM read_parquet('data/analysis/hourly_trips.parquet') """).df() fig = px.line(df, x='hour_bucket', y='trips', title='NYC Yellow Taxi 每小时订单量') fig.show()费用分布建议用箱线图而不是直接画均值。出租车费用数据存在大量极端值,比如有人坐车花了几百美元,直接把均值拉得面目全非,箱线图能让你一眼看到中位数和离群点。
5.2 地理热力图:kepler.gl 与 Folium
热点区域的地图,我推荐用kepler.gl,在Jupyter里使用非常方便,支持一次性加载几十万个点的聚合渲染。做法是先把聚合结果导成带经纬度或Zone的CSV,再用Python接口加载并选择heatmap图层:
from keplergl import KeplerGl map_1 = KeplerGl(height=600) map_1.add_data(data=df_zones, name='nyc_taxi_hotspot') map_1.save_to_html(file_name='nyc_taxi_hotspot.html')如果只需要把最终结论固定成报告插图,用Folium轻量一些,可以把Zone边界和热力图叠加在一起。两条路线我都试过:交互探索用kepler.gl,输出报告插图用Folium,分工明确。
这里有个小提醒:画地理数据前,一定要检查经纬度里有没有(0, 0)这类脏值。我见过很多人把整份数据一股脑丢进地图,结果发现一堆点齐刷刷落在非洲西海岸附近的海洋里,看着特别诡异——其实就是缺失坐标被填成了0。
6. 踩过的坑与排查技巧速查
6.1 典型问题排查表
把这轮踩过的坑整理成一张速查表,可以直接当参考:
| 症状 | 原因 | 解决办法 |
|---|---|---|
| 查询报Out of Memory | DuckDB内存上限不够,或返回结果集过大 | 设置memory_limit和temp_directory;先聚合再取数 |
| 日期字段全是NULL | CSV里是“1/15/2023 14:30”这类格式 | 用STRPTIME手动转换,或设置dateformat参数 |
| 同一个月文件读两遍结果不一致 | 文件里有重复表头行 | 先head或grep检查文件头尾,再调整skip参数 |
| 带时间条件的SQL仍然很慢 | 查询没有走分区裁剪 | 用EXPLAIN查看实际扫描的文件,确认SQL带year/month过滤 |
| 地图画出来一堆点在海洋里 | 坐标存在(0,0) | 绘图前过滤掉trip_distance<=0或经纬度为0的记录 |
6.2 几条独家避坑经验
第一,先跑一个月,再跑全量。我强烈建议把最小可用数据范围先跑通:下载、建表、聚合、出图,全流程走一遍再放开全量。200GB的数据出问题时排查起来极其痛苦,但一个月的数据几十秒就能暴露问题。
第二,下载文件时别光顾着并发拉取。我用的wget脚本确实快,但中途也有文件损坏的情况。建议下完后统一核对文件大小,或者用脚本数一下每个文件的行数,否则会在读取CSV阶段遇到各种奇怪的解析错误,排查起来很浪费时间。
第三,对TLC这类多年份数据集,字段不一致的问题几乎一定会遇到。我当时的做法是按年份分别建表,统一列名后再合并。后来发现如果是DuckDB,用UNION BY NAME关键字可以自动按列名对齐,能省不少手工活。这个坑虽然不大,但第一次撞上时确实让人摸不着头脑。
第四,分析结果的落地也很重要。聚合出的千万级结果不要直接导进Excel,可以先缩到Zone、小时这种粒度,再输出成Parquet或CSV。这样后续可视化、汇报、交接都会轻松很多。
项目跑到最后,我最大的感受是:200GB听起来吓人,真正的难点不在“大”,而在“乱”。下载、清洗、转格式花了大半天,后面的统计和可视化反而顺畅得出乎意料。如果下次再碰类似的数据集,我一定会先把每个月数据的schema差异和脏值情况摸清楚再动手。如果你也在折腾真实的大数据练手项目,强烈建议试试“单机+DuckDB+Parquet”这条路线,处理几百GB的公开数据集真的绰绰有余。
本文还有配套的精品资源,点击获取