1. 项目缘起与整体设计思路1.1 为什么选旅游景点数据做练手项目做数据处理练手选数据集比选工具重要得多。我试过拿公开的电商订单、气象记录、城市交通流量来练最后发现旅游景点数据是最适合入门到进阶过渡的题材。原因很直接它的字段结构天然混合了数值型、分类型、文本型和地理型数据一张表里就能把数据清洗、聚合、分组、可视化全流程跑一遍不用来回换数据集。这次拿到的是一份某年河北省旅游景点数据原始字段大概包含景点名称、所在城市、景区等级、门票价格、游客评分、评论数量、开放时间、景点类型等。数据量不算大几千条量级但脏数据一点不少——价格字段混着“免费”“旺季80淡季60”这种文本评分有空白也有“暂无评分”城市名有“石家庄”“石家庄市”“石家庄 市”三种写法。这种“半脏不脏”的状态其实最贴近真实业务场景比那种完全干净的玩具数据集有价值得多。这个项目能做什么说白了就是回答几个实际问题河北哪些城市的旅游资源最集中5A和4A景区的门票定价有没有明显分层游客评分和评论数之间是不是正相关不同类型的景点自然风光、历史人文、主题乐园在评分上有没有系统性差异这些问题用Excel也能凑合答但一旦涉及分组聚合、多条件筛选、交叉分析Python的效率优势就出来了。适合谁来参考如果你已经会写基本的Python循环和函数但一碰到pandas的groupby就发怵或者matplotlib画出来的图总是丑得没法放进汇报材料那这个项目的完整流程正好可以照着走一遍。我下面会把每一步的操作意图、参数选择和踩过的坑都写清楚你拿自己的数据替换也能跑通。1.2 技术栈选型与背后的取舍逻辑工具选型上我没有追求“全而新”而是按“够用、稳定、可复现”三个标准来定。核心组合是pandas numpy matplotlib seaborn地理分布那块如果要做地图就加pyecharts或plotly。先说pandas。有人会问为什么不直接用SQL或者Excel Power Query。SQL适合数据在数据库里的场景但这份数据是Excel/CSV文件导入导出反而多一道手续Power Query做清洗可以但一旦要做复杂的自定义聚合和可视化联动还是得回到代码。pandas的read_excel和read_csv能直接吃文件groupbyagg的组合能覆盖90%的分析需求这是它不可替代的地方。numpy在这里主要做数值计算兜底比如价格字段清洗完后做分箱、算分位数用np.percentile比手写排序取中位数稳得多。matplotlib是绘图底座seaborn建在它上面做统计类图表箱线图、小提琴图、回归图代码量能少一半。我个人的习惯是探索阶段用seaborn快速出图看分布最终出报告时用matplotlib精细调样式两者配合着来。地理可视化这块要单独说。如果只是看各城市景点数量的对比普通柱状图就够了但如果想在地图上按城市位置标点、用颜色深浅表示数量就得用pyecharts的Geo图或者plotly的scatter_mapbox。pyecharts生成的是HTML文件分享方便但样式定制不如matplotlib灵活plotly交互性强但数据量大时渲染会卡。我的建议是先想清楚这张图给谁看、在什么场景下看再决定用哪个工具不要为了炫技硬上地图。提示不要一上来就装一堆用不到的库。先把pandas、matplotlib、seaborn、openpyxl这四个装好跑通全流程后再按需加库。环境越干净出问题越好排查。2. 数据清洗与核心细节解析2.1 字段类型识别与缺失值处理策略拿到数据第一步不是急着画图而是把每个字段的“真实类型”和“业务含义”对齐。我习惯先用df.info()和df.head(20)扫一遍再用df.describe(includeall)看数值和分类字段的分布。这一步能快速暴露三类问题类型错位、缺失值、异常值。以门票价格为例原始列里混着“免费”“60”“80元”“旺季100淡季50”这些值。直接astype(float)会报错得先做文本清洗。我的处理逻辑是分三步走第一步把“免费”“免票”统一替换成0第二步用正则提取字符串里的第一个数字比如“旺季100淡季50”提取出100“80元”提取出80第三步对提取结果做合理性检查价格小于0或大于1000的标记为异常人工复核。import pandas as pd import re def clean_price(x): if pd.isna(x): return None x str(x).strip() if x in [免费, 免票, 免费开放]: return 0.0 nums re.findall(r\d\.?\d*, x) if nums: return float(nums[0]) return None df[price_clean] df[门票价格].apply(clean_price)这段代码的关键在于re.findall只取第一个数字。为什么因为大部分“旺季X淡季Y”的写法里旺季价格更能代表景区的定价水平而且取第一个数字比取平均值更符合业务直觉。当然这是基于常见实践的判断如果你的分析目标是淡季客流那就该取第二个数字。缺失值处理要分字段看。评分字段的缺失不能简单用均值填充因为“暂无评分”本身可能意味着新景区或冷门景区填充均值会掩盖这个信息。我的做法是保留缺失在分析时单独标注“无评分景区”的占比。评论数量的缺失则可以用0填充因为没评论约等于评论数为0这个逻辑是通的。城市名称的标准化是个体力活但必须做。“石家庄”“石家庄市”“石家庄 市”要统一成“石家庄”否则groupby会把它们当成三个不同的城市。我一般用str.replace配合正则去掉“市”字和空格再对照一份标准城市列表做校验。2.2 异常值识别与处理的分寸感异常值处理最忌讳“一刀切”。门票价格里出现一个9999元的记录可能是数据录入错误也可能是某个特殊体验项目的真实价格。我的原则是先标记再判断不轻易删除。用IQR方法可以快速定位异常值Q1 df[price_clean].quantile(0.25) Q3 df[price_clean].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR outliers df[(df[price_clean] lower) | (df[price_clean] upper)]跑完之后不要急着drop先看看这些异常值对应的景点名称。如果发现是知名景区的高价体验项目那就保留如果是明显的数据错误比如价格是负数那就修正或删除。我踩过的坑是有一次直接把所有IQR之外的记录删了结果把一个真实的高端度假区数据也删掉了导致后续分析里那个城市的平均票价明显偏低。评论数量字段也有类似问题。有些景区的评论数特别高可能是热门打卡地也可能是数据采集时把多个平台的评论重复累加了。这时候可以结合评分一起看如果评论数极高但评分很低大概率是数据问题如果评论数和评分都高那就是真实的热门景区。注意清洗完的数据一定要另存一份不要在原表上直接改。我习惯用df_clean df.copy()开头这样随时能回到原始数据对照。3. 实操过程与核心分析环节3.1 城市维度聚合谁才是河北旅游的“头部城市”城市维度的分析是最直观的切入点。用groupby按城市聚合算每个城市的景点数量、平均票价、平均评分、总评论数一张表就能看出资源分布。city_stats df_clean.groupby(city).agg( 景点数量(name, count), 平均票价(price_clean, mean), 平均评分(rating, mean), 总评论数(comments, sum) ).sort_values(景点数量, ascendingFalse)跑出来的结果通常呈现明显的“二八分布”少数几个城市集中了大部分景点其余城市数量很少。这时候不要只盯着数量看要结合票价和评分一起解读。比如某个城市景点数量多但平均评分低可能是同质化严重另一个城市景点少但评分高可能是精品路线。我一般会再做一步“密度”计算用景点数量除以该城市的行政区面积得到每万平方公里景点数。这个指标能排除城市面积差异的干扰更公平地比较旅游资源密度。面积数据可以从公开的统计年鉴里找手动补一个小字典就行。area_dict {石家庄: 14530, 保定: 22185, 张家口: 36800, ...} city_stats[景点密度] city_stats[景点数量] / city_stats.index.map(area_dict) * 10000这一步的实操心得是面积数据不用追求特别精确量级对就行。因为我们的目的是做相对比较不是做精确的地理测算。3.2 景区等级与票价的交叉分析景区等级5A、4A、3A等和票价之间的关系是很多人关心的点。直觉上5A应该比4A贵但实际数据往往不是这么简单。用箱线图看分布最直观import seaborn as sns import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False sns.boxplot(xlevel, yprice_clean, datadf_clean, order[5A, 4A, 3A, 2A]) plt.title(各等级景区门票价格分布) plt.xlabel(景区等级) plt.ylabel(门票价格元) plt.show()从经验来看5A景区的票价中位数确实高于4A但5A的分布范围也更宽——有免费的5A比如某些红色旅游景点也有两三百的5A。4A景区反而集中在50到120这个区间分布更紧凑。3A及以下景区里免费的比例明显更高。这个发现的实际意义是如果你在做旅游产品定价参考不能只看等级还要看景区类型。自然风光类的5A往往比人文类的5A票价高因为前者有索道、观光车等附加消费。我还会算一个“性价比”指标评分除以票价。这个指标虽然粗糙但能快速筛出“便宜又好玩”的景区。计算时要注意票价为0的情况直接返回一个很大的值或者单独标记。3.3 评分与评论数的相关性验证评分和评论数之间的关系我一开始以为是正相关——好评多所以评论多。但实际跑出来散点图往往呈现“漏斗形”评论数少的景区评分差异很大有5.0也有3.0评论数多的景区评分集中在4.0到4.8之间。sns.scatterplot(xcomments, yrating, datadf_clean, alpha0.5) plt.xscale(log) plt.title(评论数与评分关系) plt.xlabel(评论数对数刻度) plt.ylabel(评分) plt.show()用对数刻度是因为评论数的分布跨度太大从个位数到几万都有。线性刻度会把大部分点挤在左边看不清。这个现象的解释是评论数少的景区样本量小评分容易走极端评论数多的景区经过大数定律的“平滑”评分趋于稳定。所以看评分时一定要结合评论数一个只有3条评论的5.0分景区参考价值远不如一个有3000条评论的4.5分景区。实操中我会加一条“置信度”规则评论数少于50的景区评分仅供参考不纳入排名类分析。这个阈值可以根据数据量调整数据量大就提高到100数据量小就降到30。3.4 可视化图表的组合与排版单张图能说明一个问题但一份完整的分析报告需要多张图组合叙事。我的排版逻辑是先总后分先宏观后微观。第一组图城市景点数量柱状图 城市平均评分柱状图并排摆放一眼看出“量”和“质”的分布是否一致。第二组图景区等级与票价的箱线图 景区类型的饼图展示结构特征。第三组图评分与评论数的散点图 票价与评分的散点图展示变量间关系。fig, axes plt.subplots(2, 2, figsize(14, 10)) sns.barplot(xcity_stats.index[:10], ycity_stats[景点数量][:10], axaxes[0,0]) axes[0,0].set_title(景点数量Top10城市) axes[0,0].tick_params(axisx, rotation45) sns.boxplot(xlevel, yprice_clean, datadf_clean, axaxes[0,1]) axes[0,1].set_title(各等级票价分布) sns.scatterplot(xcomments, yrating, datadf_clean, alpha0.4, axaxes[1,0]) axes[1,0].set_xscale(log) axes[1,0].set_title(评论数与评分) type_counts df_clean[type].value_counts() axes[1,1].pie(type_counts.values, labelstype_counts.index, autopct%1.1f%%) axes[1,1].set_title(景区类型占比) plt.tight_layout() plt.savefig(analysis_overview.png, dpi150, bbox_inchestight) plt.show()bbox_inchestight这个参数一定要加否则保存的图片边缘会被裁掉。dpi150是打印和屏幕显示的平衡点再高文件太大再低看着模糊。提示中文字体问题几乎是每个人都会踩的坑。Windows上用SimHeiMac上用Arial Unicode MSLinux上如果没有中文字体就下载一个思源黑体放到matplotlib的字体目录里。设置完字体后记得把axes.unicode_minus设为False否则负号显示成方块。4. 常见问题与排查技巧实录4.1 数据读取阶段的典型报错问题一Excel文件读取时列名错位。有些Excel文件前几行是标题说明不是真正的表头。pd.read_excel默认从第一行读结果列名变成“Unnamed: 0”。解决办法是用skiprows参数跳过前几行或者用header参数指定表头行号。df pd.read_excel(data.xlsx, skiprows2, header0)问题二CSV文件中文乱码。这是编码问题常见的有utf-8、gbk、gb18030三种。我一般先试utf-8报错就换gbk再不行就gb18030。pd.read_csv(data.csv, encodinggbk)。问题三数字列被读成字符串。比如价格列里有“暂无”这种文本pandas会自动把整列识别为object类型。这时候不要急着转换先按前面说的清洗逻辑处理文本再统一转float。4.2 分组聚合时的常见陷阱陷阱一groupby后索引丢失。默认情况下groupby的键会变成索引后续操作不方便。加as_indexFalse可以保留为普通列。city_stats df_clean.groupby(city, as_indexFalse).agg(...)陷阱二agg函数对多列应用不同聚合方式。用字典传参时键是列名值是聚合函数。但要注意列名必须存在否则会报KeyError。df_clean.groupby(city).agg({ name: count, price_clean: mean, rating: mean })陷阱三mean()自动忽略NaN。这是pandas的默认行为大部分时候是好事但如果你想知道缺失比例就得用lambda x: x.isna().mean()单独算。4.3 可视化阶段的样式调整问题一x轴标签重叠。城市名太长时柱状图的标签会挤在一起。解决办法是旋转45度或90度plt.xticks(rotation45, haright)。haright让标签右对齐视觉上更整齐。问题二图例位置遮挡数据。默认图例在右上角经常挡住柱子。用plt.legend(locupper left, bbox_to_anchor(1, 1))把图例移到图外。问题三颜色太多显得乱。seaborn的默认调色板在分类多的时候会显得花哨。我一般用paletteSet2或palettemuted颜色柔和且区分度够。4.4 常见问题速查表问题现象可能原因排查方法解决方案读取Excel列名错位表头不在第一行打开文件看前几行用skiprows或header参数中文显示为方块未设置中文字体检查rcParams设置SimHei或思源黑体groupby结果为空分组键有空格或大小写差异打印unique值先做strip和lower标准化散点图点重叠严重数据量大且值集中看数据分布加alpha透明度或改用hexbin保存图片边缘被裁未设置bbox_inches打开图片检查加bbox_inchestight价格转换报错文本中含非数字字符打印原始值用正则提取数字后再转换4.5 几个让我印象深刻的踩坑经历有一次做城市聚合发现“秦皇岛”的景点数量异常少但印象里秦皇岛旅游资源很丰富。排查后发现原始数据里写的是“秦皇岛市”而我的标准城市列表里是“秦皇岛”groupby时被分成了两组。这种问题在数据清洗阶段不暴露一到聚合就现形。后来我养成了一个习惯groupby之前先打印分组键的unique值肉眼扫一遍有没有明显重复。还有一次画箱线图发现5A景区的票价箱体特别扁以为数据有问题。后来才意识到是因为5A景区里有几个免费景点把整体分布拉低了箱体自然就扁。这不是数据错误而是真实分布特征。所以看图时不要急着下结论先想想业务上是否合理。评论数用对数刻度这个技巧是我在做了好几次散点图都觉得“看不出东西”之后才悟出来的。线性刻度下评论数上万的点和评论数十的点挤在一起完全看不出趋势。换成对数刻度后分布形态立刻清晰了。这个经验可以推广到所有跨度大的数值字段价格、人口、流量都值得试试对数刻度。5. 分析结论的呈现与后续扩展方向5.1 如何把分析结果写成可读的结论跑完代码只是第一步把结果翻译成业务语言才是交付价值的地方。我的写法是“数据解读建议”三段式。比如石家庄景点数量最多XX个但平均评分在省内排名中游X.X分说明资源集中但品质参差不齐。建议优先提升现有景区的服务质量和游客体验而非继续扩张数量。这种写法比单纯罗列数字有用得多。读者不需要自己从数字里找规律你直接告诉他规律是什么、意味着什么。图表的选择也要服务于结论。如果结论是“5A和4A票价差异不大”那就放箱线图如果结论是“自然风光类景区评分普遍高于人文类”那就放分组柱状图或小提琴图。每张图都要有明确的“使命”不要为了凑版面放图。5.2 这个项目还能怎么往下做当前的分析停留在描述性统计层面往下走有几个方向。一是做预测。用线性回归或随机森林以票价、等级、类型、城市为特征预测评分或评论数。这个方向能回答“什么样的景区更容易获得高评分”这类问题。特征工程时要注意把分类变量做独热编码数值变量做标准化。二是做文本分析。如果数据里有景区简介或游客评论的文本字段可以用jieba分词加词频统计看看游客最关注什么——是“风景”“服务”还是“性价比”。这个方向能把分析从结构化数据扩展到非结构化数据。三是做时间序列。如果数据包含不同年份的游客量或收入可以画趋势线、算同比增长率看旅游市场的变化趋势。这个方向对数据要求更高但分析价值也更大。四是做交互式看板。用pyecharts或plotly dash把图表串起来做成可以筛选城市、等级、类型的交互页面。这个方向适合需要反复查看数据的场景但开发成本比静态图表高不少。我个人建议先把描述性分析做扎实再往预测或文本方向扩展。因为描述性分析是地基地基不稳后面的模型再花哨也站不住。而且很多业务问题描述性分析就能回答不一定非要上机器学习。5.3 给后来者的几条实操建议第一数据清洗的时间要留够。我做过统计一个完整的数据分析项目清洗占60%的时间分析和可视化各占20%。新手最容易低估清洗的复杂度结果做到一半发现数据有问题返工成本很高。第二每一步都保存中间结果。df.to_csv(step1_clean.csv)这种操作看起来笨但出问题时能快速定位是哪一步引入的错误。我习惯用带序号的文件名step1、step2、step3一目了然。第三图表先求清楚再求好看。很多人一上来就调颜色、调字体、加阴影结果数据本身都没看明白。我的顺序是先用默认样式出图看趋势确认分析方向对了再花时间调样式。第四结论要经得起反问。每次写完一条结论自己问一句“有没有可能是别的原因”。比如“A城市评分低是因为景区质量差”有没有可能是“A城市景区数量多拉低了平均分”多想一层结论的可靠性就高一层。第五代码要能复现。不要用df.dropna(inplaceTrue)这种原地修改也不要在notebook里跳着执行单元格。从头到尾按顺序跑一遍能出结果才算真正完成。我一般会在最后加一个“一键运行”的检查把所有步骤串起来跑一次确认没有遗漏。这个项目我前后做了大概一周中间返工了两次一次是城市名没标准化一次是价格字段清洗逻辑有漏洞。但正是这些返工让我对pandas的groupby和apply有了更深的理解。如果你也在做类似的数据分析练手项目我的建议是不要怕慢把每一步都想清楚再动手比急着出图有价值得多。