旅游数据清洗与可视化实战:从字段体检到五大分析切面

📅 发布时间:2026/10/22 3:36:59
旅游数据清洗与可视化实战:从字段体检到五大分析切面
1. 拿到一份三无旅游数据时我为什么先做字段体检而不是直接画图很多人拿到一份旅游景点数据第一反应是打开绘图库先画个柱状图看看哪个城市景点多。我早年也这么干过结果图是画出来了汇报的时候被问了一句你这个数据里有多少条是重复的、有多少条地址是空的当场卡壳。从那以后我养成了一个习惯任何一份数据到手先做字段体检再谈可视化。所谓三无数据就是没有字段说明、没有数据字典、没有采集时间标注。这次拿到的这份某年河北省旅游景点数据就是典型的三无产品——它可能是从某个公开数据源导出的字段名是拼音缩写或者英文简写行数看着不少但质量未知。这种情况下直接画图等于在流沙上盖楼。字段体检要回答四个问题有多少行、有多少列、每列是什么类型、每列的缺失和重复情况如何。这四个问题不搞清楚后面所有的分析都是空中楼阁。我见过太多人跳过这一步最后在景点数量排名的图里发现某个城市数量异常高一查才发现是同一景点被采集了七八次。具体操作上我习惯用 pandas 的info()、describe()、isnull().sum()三件套先过一遍。info()看类型和非空计数describe()看数值列的分布isnull().sum()看缺失。对于这份旅游数据重点要关注的是景点名称列有没有重复、城市/地区列有没有空值、经纬度列是不是数值类型、门票价格列有没有混入免费暂无这类文本。import pandas as pd df pd.read_csv(hebei_tourism.csv, encodingutf-8) print(df.shape) print(df.info()) print(df.isnull().sum()) print(df.duplicated(subset[景点名称]).sum())这里有个坑要提醒编码问题。国内很多数据源导出的是 GBK 或 GB18030 编码直接read_csv会报UnicodeDecodeError。我的做法是先试 utf-8失败就换 gb18030再失败就用chardet探测。别小看这一步编码错了后面所有中文列都是乱码分析无从谈起。字段体检还有一个容易被忽略的点列名本身可能带空格或不可见字符。比如景点名称 和景点名称在肉眼看来一样但 pandas 会当成两列。我一般会用df.columns df.columns.str.strip()统一清理一遍避免后续按列名取值时莫名其妙报 KeyError。体检做完你会对这份数据有一个基本判断它能不能支撑哪个城市景点最多门票价格分布如何景点类型占比这类分析。如果关键列缺失严重比如城市列空了三分之一那就要考虑是补全还是放弃这个维度。先判断数据能不能用再决定怎么用这是数据处理的基本纪律。2. 清洗这份旅游数据时我踩过的四类脏数据和处理策略字段体检只是摸底真正花时间的是清洗。这份旅游景点数据里我遇到了四类典型的脏数据每一类的处理方式都不一样处理错了会直接影响后续可视化的结论。2.1 景点名称重复是同一景点还是同名不同地第一类问题是景点名称重复。比如某某山在河北可能不止一个地方有直接按名称去重会把不同城市的景点误删。我的策略是组合去重用景点名称城市作为联合主键判断重复而不是单看名称。df[联合键] df[景点名称].astype(str).str.strip() _ df[城市].astype(str).str.strip() df df.drop_duplicates(subset[联合键], keepfirst)keepfirst保留第一条是因为通常第一条是原始记录后面的可能是采集重复。但如果数据里有更新时间列应该保留最新的那条。这里没有时间列所以保留第一条是合理选择。2.2 门票价格混入文本正则提取比强制转换靠谱第二类问题是门票价格列。这一列里混着80元免费暂无60-120元等各种格式。直接astype(float)会报错用errorscoerce又会把免费变成 NaN丢失信息。我的做法是分两步先用正则提取数字再单独标记免费和暂无。import re def parse_price(x): x str(x).strip() if 免费 in x: return 0.0 nums re.findall(r\d\.?\d*, x) if not nums: return None return float(nums[0]) df[门票价格_数值] df[门票价格].apply(parse_price)对于60-120元这种区间我取的是下限。为什么取下限而不是平均值因为旅游决策里游客看到的价格区间通常以下限作为起步价参考取下限更符合实际感知。这是一个业务判断不是技术判断但恰恰是这种判断决定了分析结果的可信度。2.3 城市列的空值与别名统一映射是必须的第三类问题是城市列。河北有11个地级市但数据里可能出现石家庄石家庄市石家庄地区三种写法还有的直接空着。空值如果占比不高可以考虑用地址列反推如果占比高这个维度就只能放弃。别名统一我用字典映射city_map { 石家庄市: 石家庄, 石家庄地区: 石家庄, 唐山市: 唐山, # ... 其余城市同理 } df[城市] df[城市].replace(city_map)这里有个经验映射字典要一次性建全不要边做边补。我一般会先df[城市].unique()把所有取值打印出来人工过一遍再写映射。否则做到一半发现漏了一个别名前面的分组统计全要重跑。2.4 经纬度异常超出河北范围的直接标记第四类问题是经纬度。河北的经纬度大致范围是东经113°到120°北纬36°到43°。超出这个范围的坐标要么是采集错误要么是景点在外地但被归到了河北。我的处理是标记出来不直接删因为可能是数据源本身的问题删了会掩盖问题。df[坐标异常] ~( df[经度].between(113, 120) df[纬度].between(36, 43) ) print(df[df[坐标异常]][[景点名称, 城市, 经度, 纬度]])标记出来之后我会人工看一眼这些异常记录。如果只是个别几条删掉如果成片异常那说明数据源有问题需要在报告里说明。清洗这一步的核心原则是每一步都要可追溯。我习惯在清洗过程中记录每一步删了多少行、改了多少值最后汇总成一张清洗日志表。这样别人问你数据怎么从5000条变成4200条的你能一条条说清楚。没有清洗日志的分析是不可复现的分析。3. 从景点数量到门票分布我常用的五个分析切面数据清洗完才轮到分析。这份河北旅游景点数据我通常会从五个切面切入每个切面回答一个具体问题而不是笼统地分析一下。3.1 城市维度景点数量排名与密度第一个切面是城市维度的景点数量。用groupby(城市).size()就能得到排名。但光看数量不够还要看密度——用景点数量除以该城市面积才能反映旅游资源的集中程度。city_stats df.groupby(城市).agg( 景点数量(景点名称, count), 平均票价(门票价格_数值, mean) ).sort_values(景点数量, ascendingFalse)这里要注意平均票价要用清洗后的数值列不能用原始文本列。而且平均票价对异常值敏感如果有个别景点票价几百元会拉高平均值。所以我会同时算中位数两个数差得远就说明分布偏斜。3.2 门票价格维度免费景点占比与价格分档第二个切面是门票价格。我关心两个数免费景点占比和价格分档分布。免费景点占比反映旅游惠民程度价格分档反映景点定位。分档我一般按 0元、1-50元、51-100元、100元以上四档。分档边界不是拍脑袋定的而是参考了国内景区门票的常见定价区间50元以下多为城市公园类50-100元多为自然景区100元以上多为大型主题景区或5A景区。bins [-1, 0, 50, 100, float(inf)] labels [免费, 1-50元, 51-100元, 100元以上] df[价格档位] pd.cut(df[门票价格_数值], binsbins, labelslabels) price_dist df[价格档位].value_counts()3.3 景点类型维度自然景观与人文景观的比例第三个切面是景点类型。如果数据里有类型列直接分组如果没有就要从景点名称里做关键词匹配。比如名称含山湖河森林的归为自然景观含寺庙塔纪念馆遗址的归为人文景观。natural_kw [山, 湖, 河, 森林, 公园, 峡谷, 溶洞] human_kw [寺, 庙, 塔, 纪念馆, 遗址, 故居, 博物馆] def classify(name): name str(name) if any(k in name for k in natural_kw): return 自然景观 if any(k in name for k in human_kw): return 人文景观 return 其他 df[景点类型] df[景点名称].apply(classify)这种关键词分类法准确率不是100%但胜在快速可解释。如果要做严谨分析应该人工标注一批样本做验证算出准确率再决定是否采用。3.4 地理分布维度散点图看景点聚集带第四个切面是地理分布。用经纬度画散点图能直观看到景点是集中在某几个城市还是沿某条山脉或河流分布。河北的景点分布有个特点太行山沿线和沿海地区是两个明显的聚集带。画散点图时我会把城市用不同颜色区分这样一眼能看出哪个城市的景点在空间上更分散。如果数据量不大还可以叠加城市中心点做参照。3.5 名称词频维度从景点名看旅游宣传偏好第五个切面比较有意思对景点名称做词频统计。把名称分词后统计高频词能看出旅游宣传的偏好。比如生态文化古镇红色这些词出现的频率反映了当地旅游资源的宣传重点。from collections import Counter import jieba words [] for name in df[景点名称].dropna(): words.extend(jieba.cut(str(name))) word_freq Counter(words).most_common(30)分词结果里会有的山景区这类无意义词需要建一个停用词表过滤掉。这一步做完往往能发现一些数据本身没告诉你的信息。这五个切面不是必须全做而是根据数据质量和分析目的选择。数据支撑哪个切面就做哪个不要为了凑分析维度硬做。比如经纬度缺失严重地理分布这个切面就该放弃。4. 可视化选型什么数据配什么图我踩过的坑分析做完最后是可视化。可视化最容易犯的错是一张图打天下——不管什么数据都用柱状图。实际上数据类型决定图表类型选错了图信息传达效率会大打折扣。4.1 城市排名用横向条形图而不是纵向柱状图城市名称是中文纵向柱状图的 x 轴标签会挤在一起或者倾斜可读性差。横向条形图barh让城市名水平排列一目了然。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False city_stats[景点数量].plot(kindbarh, figsize(10, 6)) plt.xlabel(景点数量) plt.title(各城市景点数量排名) plt.tight_layout() plt.show()这里必须提醒matplotlib 默认不支持中文不设置font.sans-serif会显示方块。axes.unicode_minus False是解决负号显示问题的虽然这份数据用不上但养成习惯没坏处。4.2 价格分布用直方图加核密度曲线门票价格是连续数值用直方图看分布形态叠加核密度曲线看趋势。如果免费景点多直方图在0处会有一个高峰这是正常现象不要以为是数据错误。import seaborn as sns sns.histplot(df[门票价格_数值].dropna(), bins30, kdeTrue) plt.xlabel(门票价格元) plt.title(门票价格分布) plt.show()bins 的数量有讲究太少看不出分布细节太多会显得毛刺。一般用sqrt(n)或者 Sturges 公式估算。这份数据几千条30个 bins 是合理的。4.3 类型占比用饼图还是环形图类型占比这类构成数据饼图和环形图都行。但我更推荐环形图因为中间的空洞可以放总数标注信息密度更高。不过如果类别超过5个饼图就会显得杂乱这时候应该改用条形图。type_counts df[景点类型].value_counts() plt.pie(type_counts, labelstype_counts.index, autopct%1.1f%%) plt.title(景点类型占比) plt.show()4.4 地理分布用散点图加透明度经纬度散点图如果点重叠严重要设置alpha透明度否则密集区域会变成一团黑。颜色可以按城市映射但城市多了颜色会难区分这时候按类型映射更合适。plt.scatter(df[经度], df[纬度], alpha0.5, s20) plt.xlabel(经度) plt.ylabel(纬度) plt.title(景点地理分布) plt.show()4.5 词频用词云还是条形图词频可视化词云好看但信息不精确条形图精确但不够直观。我的做法是两个都做词云放在报告开头吸引眼球条形图放在后面提供精确数据。词云要注意中文字体路径否则也是方块。from wordcloud import WordCloud wc WordCloud(font_pathSimHei.ttf, width800, height400).generate_from_frequencies(dict(word_freq)) plt.imshow(wc) plt.axis(off) plt.show()可视化选型的核心原则是让读者用最少的认知成本获取最多的信息。图不是越花哨越好而是越清晰越好。我见过有人用3D饼图展示占比结果因为透视变形读者根本判断不出哪块更大这就是典型的为了好看牺牲了信息传达。5. 这份数据还能怎么挖三个进阶方向和我的实操建议基础分析做完如果还想深挖这份数据还有三个进阶方向可以尝试。这些方向不一定都要做但知道有这些可能性能帮你在拿到类似数据时快速判断它的价值上限。5.1 用聚类做城市旅游画像把每个城市的景点数量、平均票价、免费占比、类型比例作为特征做 KMeans 聚类可以把城市分成几类。比如高数量低票价型低数量高票价型均衡型等。这样比单纯排名更有洞察力。from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans features city_stats[[景点数量, 平均票价]].fillna(0) scaled StandardScaler().fit_transform(features) kmeans KMeans(n_clusters3, random_state42).fit(scaled) city_stats[聚类标签] kmeans.labels_聚类前必须标准化否则数量级大的特征会主导距离计算。random_state固定是为了结果可复现这在分析报告里很重要。5.2 用文本分析挖景点描述的隐藏信息如果数据里有景点描述列可以做情感分析或主题建模。比如描述里适合亲子适合徒步这类词能帮游客按需求筛选景点。这比单纯按城市和价格筛选更贴近实际使用场景。5.3 用时间维度做趋势分析如果有的话如果数据里有开放时间或采集时间可以做时间趋势。但这份数据没有时间列所以这个方向做不了。没有时间列就不要硬做趋势分析这是基本纪律。最后分享几个我在做这类分析时的实操建议。第一所有图表都要有标题和轴标签不要指望读者自己猜。第二颜色不要超过7种超过人眼就难区分了。第三分析结论要能追溯到具体数据不要写河北旅游资源丰富这种空话要写石家庄景点数量最多达XX个占全省XX%。第四代码要能一键复现把清洗、分析、可视化串成一个脚本别人拿到数据就能跑出同样的结果。这份河北旅游景点数据从字段体检到清洗到分析到可视化完整走一遍大概需要半天到一天。时间主要花在清洗上分析和可视化反而快。这也是数据工作的常态八成时间在清洗两成时间在分析。理解了这一点就不会觉得清洗枯燥了因为你知道每一步清洗都在为后面的结论打地基。