本项目是 Python 期末大作业,主题为“纽约市黄色出租车出行数据分析与智能问答系统”。项目围绕 2023 年 1 月纽约市黄色出租车行程数据展开,目标是完成从数据读取、数据质量检查、数据清洗、特征工程、可视化分析、需求预测建模到智能问答系统的完整流程。
数据主要包括黄色出租车行程 parquet 文件、出租车区域查询表 taxi_zone_lookup.csv,以及 Taxi Zone Shapefile 空间边界文件。项目使用 pandas、geopandas、matplotlib、seaborn、scikit-learn、tensorflow、streamlit 等工具完成分析与展示。
- 可交互的 Streamlit 问答页面:项目新增
app.py,用户可以在网页中输入问题、选择示例问题并查看回答结果;如果回答中包含图表路径,页面会直接展示对应图片,而不是只显示文件名。 - 规则问答与 DeepSeek 兜底结合:系统优先使用本地规则回答可计算问题,保证数字结论来自本地数据;当规则无法匹配时,再调用 DeepSeek API 提供解释性回复,并通过 Prompt 限制模型不能编造未计算的具体数值。
- 空间地图可视化扩展:项目使用 Taxi Zone Shapefile 绘制上车需求地图和高峰期上车需求地图,可以直观看到纽约市不同区域的出租车需求分布,增强了数据分析展示效果。
最终实现目标包括:
- 构建可一键运行的 Python 项目入口
main.py。 - 完成 M1 数据质量报告、数据清洗和特征工程。
- 完成 M2 时间规律、区域 TOP 10、车费影响因素和地图可视化。
- 完成 M3 区域时段需求预测数据集构造,并比较神经网络与随机森林模型。
- 完成 M4 命令行规则问答系统,并新增 Streamlit 可视化问答界面。
- 加分扩展包括 Shapefile 地图可视化和 DeepSeek API 兜底解释问答。
本项目开发过程中使用 Codex / ChatGPT 作为编程协作工具。整体方式不是一次性让 AI 生成完整项目,而是把项目拆成多个阶段,逐步提出需求、运行验证、发现问题并继续迭代。
功能拆分策略主要包括:
- 先搭建项目骨架和数据读取入口。
- 再逐步完成 M1、M2、M3、M4 各模块。
- 每个功能先实现最小可运行版本,再根据运行结果修正。
- 对复杂扩展,例如 Shapefile 地图、DeepSeek API、Streamlit 界面,单独拆分成小任务完成。
本人负责的工作包括:明确作业要求、准备数据文件、判断功能是否符合课程目标、运行程序检查错误、配置 .env、安装依赖、查看输出图表、选择是否采纳 AI 建议,以及根据报告要求补充说明文档。
其中,代码审查是本人在项目中的重要工作。AI 生成代码后,我会检查函数是否满足作业要求、路径是否使用相对路径、异常处理是否友好、输出文件是否生成在 outputs/、API Key 是否存在泄露风险,以及新增功能是否会破坏原有命令行入口。对于可视化图表、模型结果和问答输出,我也会通过本地运行结果判断代码是否真正可用,而不是只看代码表面是否完整。
本项目没有直接照搬一个完整答案,而是通过多轮对话和调试形成最终版本。AI 提供了代码草案、结构建议和错误处理思路,但每一步都结合本地运行结果进行了检查和调整。
最开始通过 AI 创建了项目基础结构,包括 main.py、data/、outputs/、requirements.txt 和 .gitignore。随后实现了黄色出租车 parquet 数据读取函数 load_trip_data(),并加入文件不存在和依赖缺失时的友好提示。
之后继续新增 load_zone_lookup() 和 load_zone_shapes(),分别读取区域查询表和 Taxi Zone Shapefile 文件。AI 提醒 Shapefile 不是单个文件,需要 .shp、.shx、.dbf、.prj、.cpg 放在同一目录。
M1 阶段主要完成数据质量报告和数据清洗。AI 帮助实现了 generate_data_quality_report(),统计每个字段的缺失值数量、缺失率和数据类型,并对乘客数、行程距离、车费和总金额进行异常值统计。
随后实现 clean_trip_data(),逐步删除时间缺失、下车时间早于上车时间、距离异常、费用异常、乘客数异常以及上下车区域缺失的记录。每一步都打印删除数量和删除比例,方便写入报告。
M2 阶段实现多类可视化:
plot_time_demand_patterns():生成小时订单量图和工作日/周末小时需求对比图。plot_top_zones():生成上车和下车 TOP 10 区域横向柱状图。plot_pickup_demand_map():基于 Taxi Zone Shapefile 绘制上车需求地图。plot_peak_pickup_demand_map():绘制高峰时段上车需求地图。plot_fare_analysis():绘制距离与车费散点图、乘客人数与车费箱线图。
AI 还帮助处理了 matplotlib 在无图形界面环境中可能报错的问题,将绘图后端设置为 Agg,保证图表可以保存到 outputs/。
M3 阶段先实现 build_demand_dataset(),按 PULocationID、pickup_dayofweek、pickup_hour 聚合订单数量,得到需求预测标签 demand_count。特征包括上车区域、星期、小时、是否周末和是否高峰。
随后实现 train_and_compare_models(),使用 train_test_split 划分训练集和测试集,使用 StandardScaler 标准化特征,并训练神经网络和随机森林模型。最终输出 outputs/model_loss_curve.png 和 outputs/model_comparison.csv。
M4 阶段先实现命令行规则问答系统 start_qa_loop(),支持至少五类问题:
- 时段查询。
- 区域排名。
- 高峰查询。
- 费用查询。
- 支付方式查询。
系统回答包含数字结论、简短解释和相关图表路径。后来又抽取出 answer_question_once(),方便命令行问答和 Streamlit 网页界面复用同一套逻辑。
AI 帮助实现了 DeepSeek API 配置读取函数 load_deepseek_config(),通过 .env 读取 DEEPSEEK_API_KEY、DEEPSEEK_BASE_URL 和 DEEPSEEK_MODEL。项目中只提交 .env.example,不提交真实 API Key。
随后实现 call_deepseek_fallback(),当规则问答无法匹配时调用 DeepSeek API 生成解释性回复。该函数不会发送完整数据集,只发送用户问题和项目上下文摘要,并明确要求模型不能编造本项目未计算的具体数值。
最后增加了 test_deepseek_connection() 和命令行特殊命令 test api,方便在课程报告中截图展示 API 连接测试。
在 Shapefile 扩展中,AI 帮助实现了读取空间边界、检查字段、合并订单统计结果和绘制分区设色图。地图输出包括:
outputs/pickup_demand_map.pngoutputs/peak_pickup_demand_map.png
这些图可以展示不同 Taxi Zone 的上车需求空间分布,以及高峰期需求热点。
采用的建议:
- 使用
pathlib.Path管理相对路径。 - 使用函数拆分不同模块,避免所有逻辑堆在
main()中。 - 对 parquet、CSV、Shapefile 读取增加友好错误提示。
- 使用
outputs/保存报告和图表。 - 使用
.env管理 DeepSeek API Key。 - 使用 Streamlit 实现 M4 可视化问答界面。
修改后采用的建议:
- AI 最初使用 Python 3.10 的
pd.DataFrame | None类型写法,后来改为更兼容 Python 3.9 的Optional[pd.DataFrame]。 - 可视化函数最初可能依赖本地图形界面,后续改为使用
matplotlib.use("Agg")。 - 规则问答中“高峰时段”问题曾被“时段查询”规则提前匹配,后来调整了规则优先级。
.gitignore最初忽略整个outputs/,后来根据提交成果需要,改为允许提交正式输出图表和 CSV。- 对 AI 生成的 Streamlit 页面,我检查了是否复用
main.py中已有逻辑,而不是重新复制一套问答规则;同时确认页面不会把.env或 API Key 暴露到代码中。 - 对 AI 生成的 DeepSeek 兜底逻辑,我重点审查了请求内容,确认只发送问题和项目摘要,不发送完整 DataFrame 或原始数据。
拒绝的建议:
- 没有把真实 API Key 写入代码。
- 没有把完整 DataFrame 发送给 DeepSeek。
- 没有让大模型直接回答未计算过的具体统计数值。
- 没有删除命令行问答系统,而是在保留
main.py的基础上新增app.py。
拒绝原因:
- API Key 属于敏感信息,必须通过
.env管理。 - 完整数据集发送给大模型会增加成本和隐私风险。
- 大模型可能编造数值,具体统计值必须由本地规则和数据计算得到。
- 保留命令行入口可以满足基础作业要求,Streamlit 作为 M4 加分扩展更合适。
AI 初稿容易把 Shapefile 当成一个普通文件来读,只检查 .shp 是否存在,例如:
if not shp_path.exists():
print("文件不存在")
zones_geo = gpd.read_file(shp_path)我在 code review 时发现这个写法不够严谨,因为 Shapefile 实际上依赖多个配套文件。如果 .shx 或 .dbf 缺失,即使 .shp 存在也可能读取失败。最终我要求改成当前 main.py 中的实现:检查 .shp、.shx、.dbf、.prj、.cpg 是否都放在 data/taxi_zones/ 目录下,并在缺失时打印具体缺少哪些文件。这样错误提示更清楚,也更符合 GIS 数据读取的实际情况。
AI 初稿中的绘图函数直接使用:
import matplotlib.pyplot as plt
plt.figure()
plt.savefig(output_path)我运行样例绘图时发现,在当前环境中 matplotlib 会尝试调用 Tk 图形界面,出现 TclError。这个错误不是数据逻辑问题,而是绘图后端依赖问题。审核后我要求改成无界面后端:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt现在 main.py 中多个绘图函数都采用这种方式,保证在没有 GUI 的环境中也能正常保存 PNG 图片。
AI 初稿的规则匹配顺序中,“时段查询”排在“高峰查询”之前。这样当用户输入“高峰时段订单量是多少?”时,因为句子里包含“时段”和“多少”,会被错误地匹配成普通小时查询。
我在测试问答样例时发现了这个问题,并把规则顺序改为先判断:
if "高峰" in question:
answer = answer_peak_question()再判断小时、区域、费用和支付方式。修正后,“高峰时段订单量是多少?”能够正确返回高峰订单量和占比,而不是返回订单最多的小时。
本节选取 M4 规则问答中的“按小时查询订单量”作为典型功能,对比 Native 版、Prompt 版和 Vibe 版三种实现方式。这个功能看起来不复杂,但实际会涉及字段检查、用户问题解析、异常输入处理、图表路径返回,以及后续是否方便接入更多问答规则,因此很适合展示三种开发方式的差异。
Native 版指完全由本人根据课程要求手写基础代码。优点是理解最深,知道每一行代码的作用;缺点是开发速度慢,尤其是 Shapefile、模型训练、API 调用和 Streamlit 页面这些模块,需要查阅较多资料。
下面的 Native 版更接近最初手写思路:先把问题中可能出现的数字拆出来,再用 if 判断。它能完成基本功能,但写法比较直接,重复判断多,输入情况覆盖不完整,可维护性也一般。
def answer_hour_question_native(df, question):
# 手写版:先假设数据里已经有 pickup_hour 字段
if "pickup_hour" not in df.columns:
return "没有 pickup_hour 字段,不能查询小时订单量。"
hour_list = []
for value in df["pickup_hour"]:
if value == 0:
hour_list.append(0)
elif value == 1:
hour_list.append(1)
elif value == 2:
hour_list.append(2)
elif value == 3:
hour_list.append(3)
elif value == 4:
hour_list.append(4)
elif value == 5:
hour_list.append(5)
elif value == 6:
hour_list.append(6)
elif value == 7:
hour_list.append(7)
elif value == 8:
hour_list.append(8)
elif value == 9:
hour_list.append(9)
elif value == 10:
hour_list.append(10)
elif value == 11:
hour_list.append(11)
elif value == 12:
hour_list.append(12)
elif value == 13:
hour_list.append(13)
elif value == 14:
hour_list.append(14)
elif value == 15:
hour_list.append(15)
elif value == 16:
hour_list.append(16)
elif value == 17:
hour_list.append(17)
elif value == 18:
hour_list.append(18)
elif value == 19:
hour_list.append(19)
elif value == 20:
hour_list.append(20)
elif value == 21:
hour_list.append(21)
elif value == 22:
hour_list.append(22)
elif value == 23:
hour_list.append(23)
counts = {}
for hour in hour_list:
if hour not in counts:
counts[hour] = 1
else:
counts[hour] = counts[hour] + 1
ask_hour = None
for i in range(24):
if str(i) + "点" in question or str(i) + "时" in question:
ask_hour = i
if ask_hour is not None:
if ask_hour in counts:
return str(ask_hour) + "点共有" + str(counts[ask_hour]) + "单。"
return str(ask_hour) + "点共有0单。"
max_hour = 0
max_count = 0
for hour in counts:
if counts[hour] > max_count:
max_hour = hour
max_count = counts[hour]
return "订单最多的小时是" + str(max_hour) + "点,共" + str(max_count) + "单。"这个版本的效率较低,因为大量时间花在手动枚举、手动计数和补条件上;但理解深度较高,因为每一步统计逻辑都由自己写出,容易知道结果是怎么来的。
Prompt 版是把明确需求一次性发给 AI,例如“实现数据质量报告函数”或“绘制 TOP 10 区域图”。这种方式效率较高,适合完成结构清楚的小功能。但如果需求没有写清楚,AI 可能会使用不完全符合项目版本或环境的写法。
下面的 Prompt 版明显比 Native 版简洁,开始使用 value_counts() 和正则表达式,不再手动枚举 24 个小时。这种代码更像一次性向 AI 提出“帮我写一个小时订单查询函数”后得到的结果:结构已经比较清楚,但和项目其他问答规则的衔接还不够完整。
import re
def answer_hour_question_prompt(df, question):
if "pickup_hour" not in df.columns:
return "未匹配到规则:缺少 pickup_hour 字段,请先完成特征工程。"
hourly_counts = df["pickup_hour"].value_counts().sort_index()
hour_match = re.search(r"(\d{1,2})\s*(点|时)", question)
if hour_match:
hour = int(hour_match.group(1))
if hour < 0 or hour > 23:
return "小时范围应在 0 到 23 之间。"
count = int(hourly_counts.get(hour, 0))
return (
f"数字结论:{hour} 点共有 {count} 单。\n"
"简短解释:该结果根据 pickup_hour 字段统计得到。"
)
busiest_hour = int(hourly_counts.idxmax())
busiest_count = int(hourly_counts.max())
return (
f"数字结论:订单最多的小时是 {busiest_hour} 点,共 {busiest_count} 单。\n"
"简短解释:该结果来自每小时订单量分组统计。"
)这个版本的效率明显提高,适合快速完成单个清晰功能;但理解深度处于中等水平,因为开发者需要继续检查 pickup_hour 是否已经转成数值、空值如何处理、图表路径是否需要返回,以及它会不会和“高峰时段”等其他规则发生匹配冲突。
Vibe 版是通过持续对话推进项目。每完成一个功能就运行检查,根据报错继续修改。例如 Streamlit 命令找不到、matplotlib 后端问题、DeepSeek 未配置兜底等,都是通过运行结果继续迭代完成的。
下面的 Vibe 版接近本项目最终采用的写法:它不是只解决“小时查询”这一个点,而是放在完整规则问答框架里。代码会先做字段与数据类型保护,再统一返回“数字结论、简短解释、相关图表路径”;同时通过规则顺序避免“高峰时段”问题被普通小时查询提前匹配。
import re
import pandas as pd
from pathlib import Path
OUTPUTS_DIR = Path("outputs")
def answer_question_once_vibe(qa_data, question):
question = question.strip()
if not question:
return "请输入一个问题。"
def answer_peak_question():
if "is_peak_hour" not in qa_data.columns:
return (
"未匹配到规则:缺少 is_peak_hour 字段,请先运行特征工程。\n"
f"相关图表路径:{OUTPUTS_DIR / 'peak_pickup_demand_map.png'}"
)
peak_flags = pd.to_numeric(qa_data["is_peak_hour"], errors="coerce").fillna(0)
peak_count = int((peak_flags == 1).sum())
total_count = len(qa_data)
peak_ratio = peak_count / total_count if total_count else 0
return (
f"数字结论:高峰时段共有 {peak_count} 单,占全部订单的 {peak_ratio:.2%}。\n"
"简短解释:高峰时段按特征工程中定义的早晚高峰小时统计。\n"
f"相关图表路径:{OUTPUTS_DIR / 'peak_pickup_demand_map.png'}"
)
def answer_hour_question():
if "pickup_hour" not in qa_data.columns:
return "未匹配到规则:缺少 pickup_hour 字段,请先运行特征工程。"
pickup_hours = pd.to_numeric(qa_data["pickup_hour"], errors="coerce").dropna()
if pickup_hours.empty:
return "未匹配到规则:pickup_hour 字段没有可用数据。"
hourly_counts = (
pickup_hours.astype("int64")
.value_counts()
.reindex(range(24), fill_value=0)
.sort_index()
)
hour_match = re.search(r"(\d{1,2})\s*(点|时)", question)
if hour_match:
hour = int(hour_match.group(1))
if not 0 <= hour <= 23:
return "小时范围应在 0 到 23 之间。"
order_count = int(hourly_counts.get(hour, 0))
return (
f"数字结论:{hour} 点共有 {order_count} 单。\n"
"简短解释:该结果按 pickup_hour 字段统计,表示这个小时内的上车订单量。\n"
f"相关图表路径:{OUTPUTS_DIR / 'hourly_demand.png'}"
)
busiest_hour = int(hourly_counts.idxmax())
busiest_count = int(hourly_counts.max())
return (
f"数字结论:订单最多的小时是 {busiest_hour} 点,共 {busiest_count} 单。\n"
"简短解释:该结果来自每小时订单量统计,可用于观察一天内出行需求峰谷。\n"
f"相关图表路径:{OUTPUTS_DIR / 'hourly_demand.png'}"
)
# 先判断高峰问题,避免“高峰时段”被普通小时规则提前匹配。
if "高峰" in question:
return answer_peak_question()
if any(keyword in question for keyword in ["小时", "时段", "几点", "点", "订单最多"]):
return answer_hour_question()
return "当前问题暂未匹配到本地规则,可交给 DeepSeek 做解释性兜底。"这个版本开发效率最高,但不是一次生成就结束,而是通过“提出需求、运行、发现冲突、修正规则顺序、补充异常处理、复用到 Streamlit 页面”的过程完成。理解深度也更均衡:AI 负责加速结构设计和代码生成,人负责判断规则边界、结果是否可信、是否符合课程项目的数据来源要求。
- Native 版理解最深,但效率最低。它适合学习基础逻辑,例如循环、字典计数和条件判断;缺点是代码冗长,后续扩展区域排名、费用分析、支付方式查询时会很吃力。
- Prompt 版效率较高,但需要人检查细节。它能快速把基础功能写得更 Pythonic,但如果提示词只描述单个函数,AI 往往不会主动考虑整个项目的规则优先级、输出格式和异常边界。
- Vibe 版最适合本项目,因为项目功能多、模块之间有依赖,需要边做边测边改。它的代码更接近最终工程实现:函数拆分更清楚,规则顺序可控,输出格式统一,也更容易被命令行和 Streamlit 页面共同复用。
最终我认为 AI 更像是协作开发助手,而不是直接替代开发者。人仍然需要负责目标判断、运行验证、安全边界和最终解释。
初始版为:
你是一个智能问答助手,请回答用户关于出租车数据的问题。
该版本过于宽泛,没有限制模型回答范围,也没有要求不能编造数值。
改进版为:
你是一个纽约市黄色出租车出行数据分析系统的助手。请根据项目中的数据分析结果回答用户问题。如果无法回答,请给出解释。
该版本加入了项目背景,但仍然没有明确说明规则系统优先,也没有明确禁止回答未计算过的具体数值。
最终版 Prompt 已记录在 system_prompt_iteration.md 中,核心要求包括:
- 模型是解释型助手。
- 不能编造项目中没有计算过的具体数值。
- 如果规则系统没有覆盖,应说明当前规则系统未覆盖该问题。
- 可以解释字段含义、分析思路、可视化含义和模型局限。
- 回答使用中文,清晰简洁,适合课程项目展示。
System Prompt 的迭代主要是为了降低大模型幻觉风险。由于本项目的数字结果必须来自本地数据计算,大模型只适合作为解释性补充。最终版 Prompt 明确了模型边界,使 DeepSeek 更适合承担 M4 的兜底问答角色。
数据质量报告:
主要可视化图表:
- hourly_demand.png
- weekday_weekend_demand.png
- top10_pickup_zones.png
- top10_dropoff_zones.png
- distance_fare_scatter.png
- fare_by_passenger_count.png
地图热力图:
模型训练结果:
规则问答示例:
- “哪个小时订单最多?”
- “18点有多少订单?”
- “上车最多的10个区域是哪些?”
- “平均车费是多少?”
- “信用卡支付占比是多少?”
DeepSeek 兜底问答示例:
- “纽约出租车行业未来会被网约车完全取代吗?”
该问题不属于规则系统可直接计算的问题,因此交给 DeepSeek 进行解释性回复,并要求不能编造本项目未计算过的具体数值。
重要提示:DeepSeek API Key 配置
如果需要体验 DeepSeek API 兜底问答功能,读者需要先参考 .env.example 在项目根目录创建
.env文件,并填入自己的DEEPSEEK_API_KEY。如果没有配置 API Key,系统仍可使用本地规则问答,但无法调用 DeepSeek 生成兜底解释。
Streamlit 可视化界面:
streamlit run app.py如果命令不可用,可使用:
python -m streamlit run app.py可交互页面截图:
AI 擅长的部分:
- 根据明确需求快速生成函数框架。
- 补充错误处理和注释。
- 组织项目结构。
- 根据报错提出修正方向。
- 辅助撰写报告和 Prompt 迭代记录。
AI 容易出错的部分:
- 可能忽略本地环境依赖是否已经安装。
- 可能使用不兼容课程要求的语法版本。
- 可能默认把具体数据分析结果交给大模型解释。
- 对文件编码、PowerShell 显示和真实文件内容的区别需要人工判断。
人类判断不可替代的部分:
- 判断哪些结果符合课程要求。
- 判断哪些数据和 API Key 不能提交。
- 判断图表是否有解释价值。
- 判断模型结果是否合理。
- 判断 AI 生成的报告是否真实反映了项目过程。
- 对 AI 生成代码进行 code review,检查逻辑是否正确、异常情况是否覆盖、依赖是否合理、运行结果是否符合预期。
本次项目后的认识变化:
我更清楚地认识到,AI 很适合做“协作式开发助手”,尤其适合帮助拆解任务、生成初版代码、补充异常处理和解释代码。但 AI 输出并不等于最终答案,仍需要人类持续运行、检查、判断和修正。真正可靠的项目不是一次生成出来的,而是在多轮反馈和验证中逐步完成的。
本项目完成了纽约市黄色出租车出行数据分析与智能问答系统的主要功能。基础部分包括数据读取、数据质量报告、数据清洗、特征工程、可视化分析和需求预测建模;扩展部分包括 Shapefile 空间地图、DeepSeek API 兜底问答和 Streamlit 可视化问答界面。
加分功能完成情况:
- 已完成 Taxi Zone Shapefile 地图可视化。
- 已完成 DeepSeek API 配置读取和兜底解释回复。
- 已完成 DeepSeek API 连接测试命令
test api。 - 已完成 Streamlit 可视化问答界面
app.py。 - 已完成 System Prompt 设计与迭代记录。
通过本次项目,我不仅完成了一个较完整的数据分析系统,也学习了如何与 AI 工具协作开发:把需求拆小、持续验证、及时修正、保留人类判断,并在安全边界内使用大模型能力。
