news 2026/9/29 1:30:32

旅游景点数据分析实战:从数据清洗到客流预测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
旅游景点数据分析实战:从数据清洗到客流预测

简介:去哪儿网国庆旅游景点数据分析实战包涵盖从数据清洗、可视化到业务洞察的完整分析流程,适合数据分析初学者、旅游行业运营人员及相关岗位求职者作为练手项目。包内共7个文件,包括5份可视化HTML报告、1份原始Excel数据集和1个Python分析脚本,压缩后仅79KB,轻量易部署。已有1379人学习下载,实践反馈良好。资源重点呈现各省份旅游景点分布热力图、景区星级分布比例饼状图、景点门票销量柱状图等典型图表,脚本基于Pandas、NumPy等库对结构化数据进行预处理与建模,可帮助读者快速掌握时间序列趋势判断、地理信息与业务数据结合的方法,并能从中提取景点受欢迎程度、高峰时段及游客评价偏好等关键洞察,进而支撑旅游规划、价格优化与精准营销等经营决策。

1. 旅游景点数据分析实战:先对目标,再跑脚本

把“旅游景点数据分析”拆开看,一半是数据工程,一半是业务问题。我遇到过景区运营负责人在周五晚上拉了三张表——门票订单、闸机过闸记录、OTA 评价,想判断“周末要不要加开夜场”。结果订单表显示 12000 人入园,闸机只有 9500 条记录,评论区还在抱怨排队。这个标题要解决的正是这类场景:把票务、客流、天气、评论等数据拿全,清洗成一套可靠的口径,再做出能指导排班和定价的分析。适合景区运营、文旅行业数据分析师、数据项目外包者,也适合准备拿真实业务练手的数据从业者。我一般把流程拆成五步:数据源选型、清洗、指标口径、可视化、结论验证。

2. 数据源与采集:门票订单、闸机客流、OTA 评论,先拿全再分析

2.1 三类数据源,选型前先想清楚“能不能复核”

旅游景点数据分析最怕的不是没数据,而是数据拿得不全或者口径对不上。常见的做法是先盘一遍“内部系统、物联设备、外部平台”这三类数据源:票务系统提供订单和支付流水,闸机/手环设备提供实际通过记录,OTA 平台提供评论和评分。它们的时间粒度和可信度不一样,用途也不同。

数据源典型字段时间粒度主要用途主要风险
票务系统订单号、票种、数量、金额、验票时间秒级收入、票种、预订趋势已支付未验票、退票记录
闸机/检票设备过闸时间、通道号、设备ID秒级入园人次、小时级客流重复计数、多人通行、设备时间偏差
OTA/点评平台评论时间、评分、内容、游玩日期分钟级口碑、满意度刷评、文本噪声
天气数据天气现象、温度、降水量小时级客流影响因素不同气象源口径不同

选型时除了看有没有字段,还要看能不能复核。比如票务系统里“已支付订单”和“已验票订单”是两个概念;闸机的“过闸次数”也不一定等于“人数”,因为团体票可能一次刷一大串。如果你拿了一个无法核对的数据源,后面做再多分析都会被质疑。

2.2 用 requests 抓一个开放接口的最小脚本

很多中小景区没有现成的数据 API,常见做法是把 Excel 导出交给分析人员统一入库。如果景区文旅局或管理方开放了数据接口,可以直接用 requests 抓取。下面是一个最小脚本结构,接口地址和访问凭证按实际环境替换即可。

import requests import pandas as pd # 接口地址按实际数据源替换;示例用保留占位 API_URL = "https://opendata.example.com/api/scenic/daily_flow" def fetch_flow(start_date, end_date, app_key): """ 拉取按天聚合的客流数据。 参数说明: start_date: 开始日期,格式 YYYY-MM-DD end_date: 结束日期,格式 YYYY-MM-DD app_key: 开放接口访问凭证 """ params = { "start": start_date, "end": end_date, "app_key": app_key, } resp = requests.get(API_URL, params=params, timeout=10) resp.raise_for_status() payload = resp.json() df = pd.DataFrame(payload["data"]) return df if __name__ == "__main__": result = fetch_flow("2025-01-01", "2025-01-07", "your_app_key") print(result.head())

代码逻辑并不复杂:params 里的 start、end 控制查询窗口,timeout=10 防止某个接口卡住把采集流程拖死;raise_for_status() 会在网络返回 4xx/5xx 时直接抛异常,避免后续拿着错误数据继续清洗。拿到数据后先 print 一下列名和头几行,确认字段名和返回结构,再存盘。

这里有个容易忽略的点:requests 默认只处理 HTTP 状态码,不处理业务层错误。有些接口即使正常返回,payload 里也可能有 error_code 字段,表示本次查询仍失败。最好在脚本中加一个判断:

data = payload.get("data") if data is None: raise RuntimeError("接口响应中没有 data 字段,请检查参数")

2.3 数据落盘:每天留一份快照,给“后悔药”留条后路

一次采集往往不是终点。票务系统、闸机系统的数据每天都在变,后台还可能补录前一天的数据,所以我会把每天拉到的内容存成带日期的文件,比如flow_20250107.csv,并在文件里加一列snapshot_date。这样,当业务方隔两周说“元旦的数据不对”时,你可以直接调出当时的快照,对比新增了一批还是原数据被修改了,不用指望数据库有自动审计。

常见做法是先把每日快照放在raw/目录,再统一读入一个分析表。不要一开始就做“一个总表不断追加”的模式,除非你确认上游字段结构永远稳定。字段一旦调整,追加模式会污染历史数据,清洗阶段只能从头再跑一遍。

提示:如果数据量不大,可以用 CSV 保留原始内容;字段嵌套复杂的 JSON,建议直接存 JSON 或 parquet,避免解析时丢失结构。

2.4 只有 Excel 导出表时,先标准化列名再合并

小景区经常没有接口,每个月从 OTA 后台导一张 Excel 表到本地。问题在于不同月份的表头顺序可能不一样,状态值也可能是“已使用”“未消费”“退票”混着写。我会先把列名统一,再 concat。

import pandas as pd from pathlib import Path # 读取同一个文件夹下所有 2025 年的销售表 files = sorted(Path("export").glob("sales_2025*.xlsx")) frames = [] for f in files: df = pd.read_excel(f, header=0) # 统一列名,避免不同月份表头顺序不同 df.rename( columns={ "订单号": "order_id", "游玩日期": "visit_date", "票种": "ticket_type", "检票时间": "check_time", "状态": "ticket_status", }, inplace=True, ) frames.append(df) raw = pd.concat(frames, ignore_index=True) print(raw.shape)

这段代码先用Path.glob匹配文件名,再逐个读取并统一列名。注意 glob 模式里的2025*.xlsx会把临时文件~$sales_2025.xlsx也匹配进来,这类临时文件通常是隐藏文件且不会被pd.read_excel正常读取,所以最好在循环里加if f.name.startswith("~$"): continue。

另外,所有数据源里一定要有一个业务主键。对票务系统是 order_id,对闸机是 record_id;如果两者都没有,至少要有“时间+通道+票号”拼接的复合键。后面去重、合并、排查都靠它。我见过有的景区导出的订单表连订单号都没有,只有身份证号和手机号,这种数据只能按人数近似,不能做订单级分析。

3. 数据清洗与指标口径:把“游客量”统一成一个数

3.1 四个常见的脏数据,先看长什么样

从数据源直接拉到 Excel 或接口数据时,大概率会遇到这几类问题:

  • 票务订单里“已支付但未验票”的订单。直接求和会高估游客量,尤其节假日很多人提前买票但不去或改期。
  • 闸机记录里,一张订单包含多张票,可能一次快速连刷;也有通道设备故障导致同一个人被记录两次。
  • 退票和改签会产生取消记录,如果按订单号去重时没先过滤状态,会保留无效行。
  • 时间字段格式混乱,有的导出列是字符串,有的精确到毫秒,还有的跨天。
  • OTA 评论里的游玩日期常是用户选的历史日期,和真正入园日期存在偏差,只能做参考。

这些脏数据不清理,后面做趋势分析会翻车。

3.2 清洗脚本:先过滤状态,再按订单去重

下面是一段常见的 pandas 清洗逻辑:

import pandas as pd def clean_ticket_records(raw_df): # 1. 只保留已核销的订单,过滤退票和未使用 df = raw_df[raw_df["ticket_status"].isin(["已使用", "used"])].copy() # 2. 按订单号去重,同一个订单保留最早一次检票时间 df = df.sort_values("check_time") df = df.drop_duplicates(subset="order_id", keep="first") # 3. 统一时间格式,并丢弃无法解析的时间 df["check_time"] = pd.to_datetime(df["check_time"], errors="coerce") df = df.dropna(subset=["check_time"]) # 4. 增加日期、小时、星期几字段,方便后面聚合 df["date"] = df["check_time"].dt.date df["hour"] = df["check_time"].dt.hour df["weekday"] = df["check_time"].dt.weekday return df

逻辑说明:第 1 步先过滤状态,是因为退票订单如果不去掉,会把订单总数和人数拉高;第 2 步 sort_values 后再 drop_duplicates,保留最早一条通过记录,能在团体票一次扫描多张票时把重复记录压到最小。第 3 步errors="coerce"会把“2025-1-2”和“2025-01-02 10:00:00”这类格式都转成 Timestamp,解析不了的变成 NaT,直接用 dropna 清掉。第 4 步是为后续按小时、按天、按周分析准备字段。

注意:如果闸机系统单独存储“通行记录”,不要直接对订单表 drop 所有重复订单。需要先确认重复的规则:同一人过闸多次是正常,只有同一订单重复过闸才需要去重。最好在业务人员协助下确认。

3.3 指标口径:入园人次、购票人数、游客量,别混着用

在旅游景点数据分析里,最常被问的一句话是“今天来了多少人?”但这背后有多个口径:

指标计算方式适用场景偏差来源
购票人数订单中票数合计收入分析、票种销售包含未到场、退票
入园人次闸机核销记录数承载量、排队管理团体票、设备误计
游客量按证件/用户唯一标识去重后人数人群画像、复游分析隐私合规、数据覆盖不全
过夜游客需要酒店或离园时间住宿率、周边消费很难全量获取,只能抽样

我一般会在分析报告开头直接写清楚“本文游客量指入园人次,口径为闸机有效核销记录”,避免被业务方反复追问。如果手头只有票务订单,可以用“验票时间非空且状态为已使用”的订单人数近似,但要在图表副标题里注明偏差。

补充一点:有些景区会把“预约人数”也当成“游客量”,预约人数只是有意向访问量,通常远大于实际入园数。如果报告里混用预约数和入园数,会得出非常离谱的结论。

3.4 有了清洗后的宽表,先做一对“验证性统计”

清洗不是直接出报表,而是先验证数量级对不对。常见做法是分别统计“订单总额”“最早和最晚入园时间”“每日合计人数”,再把某一天的人工检票记录数拿来对比。如果日报显示 10000 人,而售票窗口手工记录只有 7000 人,先别画图,回头查是预约/退票还是闸机跳码。

另外,去重和过滤的顺序不能反。如果先按 order_id 去重,同一个订单的退票状态和已使用状态会混在一起,保留下来的可能是退票。上面脚本中必须先把 ticket_status 过滤到“已使用”,再对 order_id 去重,顺序一变结果就错。这个细节看起来简单,但很容易在凌晨赶工时翻车。

4. 核心分析与可视化:客流趋势、热力分布、票种组合怎么落地

4.1 时间维度:先按小时聚合并对齐节假日

清洗后的每一条入园记录都有 check_time。最基础但最有用的操作是“按小时聚合”,它会立刻暴露景区的客流窗口。

df["date"] = pd.to_datetime(df["date"]) # date 列转成 datetime hourly = ( df.groupby([df["date"].dt.date.astype(str), "hour"]) .size() .reset_index(name="count") ) # 找出每个日期最高峰的小时 peak_hour = hourly.loc[hourly.groupby("date")["count"].idxmax()] print(peak_hour.head())

逻辑说明:groupby 括号里的日期列必须是 datetime 类型,才能用 dt.date;astype(str) 是为了分组时避免日期对象参与运算。idxmax 能直接返回每个分组最大值所在的行索引。分析结果常见的是:开园后第 1-2 小时、午饭后 13:00-15:00 各有一个高峰。如果两个峰明显,园内排班和餐饮备货的时间段就确定了。

有很多景区的数据里没有“date”字段,只有 check_time 字符串,所以要先按 3.2 的方法生成 date。不要直接对字符串做 dt.date。

4.2 空间维度:区域/项目客流占比与票种交叉表

如果闸机或验票记录里有“区域”“项目”字段,可以用 crosstab 快速看区域和票种的关系。

if "area" in df.columns and "ticket_type" in df.columns: area_ticket = pd.crosstab(df["area"], df["ticket_type"]) area_ticket["合计"] = area_ticket.sum(axis=1) area_share = area_ticket["合计"] / area_ticket["合计"].sum() print(area_ticket) print("区域占比:\n", area_share)

参数说明:crosstab 的第一个参数是行,第二个参数是列,单元格是频次。加合计列后计算区域占比,能很快识别出某个热门区域占全园 40% 以上。如果数据里还有排队时长,可以和区域客流做相关分析,判断是客流多导致排队还是通道设计问题。注意这里只是容量压力初步判断,要给出建议,需要结合运营日志进一步验证。

如果数据里没有区域字段,可以用票种替代:成人票、儿童票、年卡的比例变化能反映家庭游客和散客结构。

4.3 一张能直接交给业务方的图:双轴图

Python 数据分析与可视化里最常用的是 matplotlib 和 pandas 的绘图接口。下面画“每日入园人次 + 天气温度”的双轴图:

import matplotlib.pyplot as plt # daily_summary 是清洗后按日期聚合的 DataFrame fig, ax1 = plt.subplots(figsize=(10, 4)) ax1.bar(daily_summary["date"], daily_summary["flow"], alpha=0.7, label="入园人次") ax2 = ax1.twinx() ax2.plot(daily_summary["date"], daily_summary["temp"], color="tomato", marker="o", label="最高温度") ax1.set_xlabel("日期") ax1.set_ylabel("入园人次") ax2.set_ylabel("最高温度(℃)") fig.legend(loc="upper left") plt.tight_layout() plt.savefig("daily_flow_temp.png", dpi=150)

这段代码用 twinx 生成第二根 y 轴,左侧柱状图看入园人次,右侧折线图看温度。看趋势时注意不要在汇报里说“温度高导致人多”,只说“今年春节假期前半段人流和温度同向变化”,相关性不等于因果。图保存为 PNG 时 dpi 至少 150,不然投到屏幕会上看不清数量刻度。

如果业务方习惯 Excel 数据分析,可以把 daily_summary 输出成 CSV,让他们在 Excel 里自己做透视表复核;这样比只给一张静态图更能经得起追问。

4.4 节假日与非节假日对比,先做最简单的一组交叉

不要一上来就上模型。先做“周末/工作日”和“假期/非假期”的对比,再考虑天气。

df["is_weekend"] = df["date"].dt.dayofweek >= 5 # 如果业务方提供 holiday_list,可以再生成 is_holiday 列 df["is_holiday"] = df["date"].isin(pd.to_datetime(holiday_list).date) grouped = df.groupby(["is_weekend", "is_holiday"])["order_id"].count() print(grouped)

如果发现“工作日+非假期”的日均人数只有“周末+假期”的三分之一,那排班基本可以按这个倍数估算。节假日字段维护起来有点费事,可以先不放全年,而是选几个重点假期前后两周做对比。

我见过有人把天气、节假日、票种、OTA 评分直接丢进回归模型,想预测明天客流。结果模型精度很低,原因不是算法不好,而是连“周末/非周末”这个最核心的维度都没先聚合对比。对景区这种强周期性场景,基线预测可以先简单用“过去 4 周同时段平均”,多数时候已经够用。

5. 旅游景点数据分析实战避坑:高频翻车点和排查流程

5.1 订单量和闸机量对不上,先别急着改数据

现象:票务系统导出 12000 个订单,闸机记录只有 9500 条。业务方说“实际入园 9500”,票务坚持“卖了 12000 张票”,两边在会议室吵起来。

原因:订单表里已支付但未验票、退票改签、团体票一次闸机扫描多张票、设备跳码都可能导致差异。

解决:先做两步:第一步,把订单表按状态字段拆成已支付、已使用、已取消三组,统计各自数量;第二步,把闸机记录按时间窗口和订单号匹配。如果订单号匹配不上,再看是不是有 OTA 分销商订单号前缀。最后报告里同时写“购票 12000,入园核销 9500,其中未到 2100,设备/通道误差 400”。

5.2 把下单日期当成游玩日期,节假日趋势直接失真

现象:分析“游客量日趋势”时,发现中秋前三天人流突然大涨,但景区现场并没有这么多人。

原因:很多游客提前几天买票,票务表里默认的日期是下单时间,不是游玩时间或验票时间。按订单日期聚合,会把主动买票行为当成到访行为。

解决:优先使用验票时间和游玩日期;如果没有验票时间,至少先用游玩日期。如果只能拿到下单时间,必须在报告里标注“本趋势代表预订趋势,不代表入园客流”。否则分析结果会误导排班。

5.3 跨天和夜场,让“0 点后客流”看起来像幽灵数据

现象:清洗后统计每日客流,发现每天 0 点到 1 点还有几百条入园记录,运营说这个时段根本没有游客入园。

原因:景区营业到 22 点,但闸机服务器时区设置成了 UTC;或者票务系统把凌晨 1 点的退票确认时间当成入园时间。另一种常见情况是景区有跨年/夜场活动,凌晨离场记录被记到当天。

解决:先定义“营业日”。如果景区营业到次日凌晨 2 点,就统一把凌晨 4 点前的记录归入前一日。可以写一个时间偏移函数:

def business_date(dt): # 凌晨4点前算前一天,避免夜场记录落到错误自然日 if dt.hour < 4: return (dt - pd.Timedelta(hours=4)).date() return dt.date()

逻辑是先把时间轴平移到凌晨 4 点为一天的起点,再用平移后时间取日期。这样既能处理跨夜活动,也不会把正常凌晨入场的特殊游客丢掉。参数“4”可以根据景区实际闭园时间调整。

5.4 天气字段来源不一致,导致下雨天反而不降客

现象:把天气数据 merge 到客流表后,发现“雨天客流比晴天还高”,完全违反直觉。

原因:天气数据来源有多个,有的源按城市天气,有的按景区所在地天气;有的字段是“全天天气现象”,有的只记录了 08 点的天气。如果用错,下雨天可能记录成晴天。

解决:固定使用景区最近监测站的小时天气数据,并按日期与入园时间取当日 12 点的天气作为标签;如果无法确定精度,就先不把天气作为分析主因素,只在报告里作为参考。千万不要拿不同来源的天气字段混用。

5.5 排查流程:从结果反推清洗步骤

现象:分析结果和手工统计总是差 5%,又找不到是哪个环节。

原因:清洗步骤太多,每一步都会影响行数和指标,没有在中间设置检查点。

解决:我习惯在清洗脚本里每步都打印当前行数、订单数、时间跨度,并保留一份未清洗的原始数据。例如在 3.2 的函数里加一段:

print("过滤状态后:", df.shape) print("去重后:", df.shape) print("清洗时间字段后:", df.shape)

然后准备一个人工可核验的小区间,比如 1 月 3 日某闸机的记录数,从原始数据逐步骤核对行数。哪一步和人工统计对不上,就去查那一步的规则。这个方法比直接盯着最终结论找原因高效很多。不要盲目相信系统导出的汇总表,尤其是那些带“自动去重”“智能识别”字样的导出功能,黑匣子逻辑最容易埋坑。

6. 让分析结果能指导排班和定价:一个最小验证方法

分析做出来后,验证它真的能指导业务,是最后也是最重要的一步。常见做法不是直接预测明天的精确人数,而是做一个“周末 vs 工作日、晴天 vs 雨天”的二维对比,确认最基础的两个变量是否成立。

# df_used 是清洗后的入园明细 df_used["is_weekend"] = df_used["date"].dt.dayofweek >= 5 pivot = df_used.pivot_table( index="weather_condition", columns="is_weekend", values="visitor_id", aggfunc="count", margins=True, ) print(pivot)

pivot_table 的 values 可以用“游客ID”或“订单号”计数,aggfunc="count" 统计行数。weather_condition 需要先清洗成“晴/雨/阴”等有限分类,不要直接用文本。如果二维交叉表里,“周末+晴天”的人数是“工作日+雨天”的 3 倍以上,那运营的排班和备货就能直接按这个倍数调整。

我自己的教训是:有一年做国庆排班预测,直接用了近 30 天平均客流,结果完全没把“长假前三天是高峰,后三天回落”这种节奏算进去,最后预测量比实际少了一半。后来我把验证方法固定为:先看周度周期性,再做节假日因子校准,最后才用更复杂的模型。这样至少不会在基础问题上翻车。

最后给一个具体技巧:把每次分析报告里用到的口径、数据源、清洗规则写成一个“口径说明”段落,放在图表下方。这样三个月后回看时,还能知道当时的结论是在什么前提下算出来的。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 1:30:16

C51单片机PWM舵机控制:无硬件PWM的精准角度实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:29:52

功能安全架构设计:物理边界、异构冗余与确定性状态机

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:29:28

Claude Code官方插件市场claude-plugins-official全解析:从安装到实战

1. 从标题到落地&#xff1a;claude-plugins-official 到底解决了什么问题第一次看到claude-plugins-official这个仓库名的时候&#xff0c;我下意识以为它又是一个第三方维护的插件合集&#xff0c;点进去才发现这是官方亲自下场维护的插件注册中心。这件事的意义比表面看起来…

作者头像 李华
网站建设 2026/9/29 1:29:28

FreeRTOS移植到Cortex-M芯片全攻略:原理、实操与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 1:29:24

SST固态变压器技术漫谈【12】固态变压器(SST)在智能配电网、光储并网、轨道交通牵引、微网离网四类典型场景下的差异化设计

摘要:本文系统梳理固态变压器(SST)在智能配电网、光储并网、轨道交通牵引、微网离网四类典型场景下的差异化设计取向,并给出 10 kV / 1 MVA 三级式 SST 的完整逐级设计算例(含整流、隔离、逆变各级参数计算与效率预算),同时提供故障速查处置表、关键公式与工程取值速查卡…

作者头像 李华
网站建设 2026/9/29 1:28:45

Agentic编排运行时ax:基于Kubernetes的多Agent调度与状态管理实践

1. 从"ax"这个标题说起&#xff1a;一个被低估的运行时缩写第一次看到"ax"这个标题&#xff0c;很多人会一头雾水。它太短了&#xff0c;短到像是某个内部代号&#xff0c;或者某个命令行工具的简写。但结合热搜词里的agentic、orchestration、runtime、Ku…

作者头像 李华