尧图精选

用Pandas处理葡萄酒评论数据:CSV清洗、合并与分组分析

🕒 发布时间:2026/10/2 10:03:37 📁 来源:尧图网络
简介这是一份面向葡萄酒数据分析、文本挖掘与可视化练习的数据集包含十三万余条专业品鉴记录覆盖品种、产地、酒庄、价格及评论描述等关键字段既适合入门者练习数据清洗与统计也能支撑自然语言处理或推荐系统等进阶项目。资源共三个文件两个CSV表格文件可用于批量分析与统计一个JSON文件适合做结构化节点解析压缩包体积约二十六点八四兆字节整体层级清晰、便于按需取用。目前已有一百零二人浏览学习。借助该数据集读者能获得完整的葡萄酒评论样本库进行价格分布、评分关联、产地特征等探索性分析也可提取评论文本开展情感分析或主题建模两份CSV可对比不同数据规模下的处理性能JSON文件则适合演示嵌套数据的转换实践实用价值较高。1. 葡萄酒评论数据集130k条记录能挖出什么怎么挖拿到这份葡萄酒评论数据集第一反应是我手头终于有一份够大的、可以直接用的真实数据了。三个 CSV 文件合计 13 万条以上记录覆盖的维度不只是酒的名字和分数还带着国家、地区、酒庄、价格、评论描述这些字段。对做数据分析、数据清洗练手或者想写一篇葡萄酒主题可视化文章的人来说这份数据的价值在于它足够真实而且自带坑——缺失值、重复记录、长尾分布全都有练手和实战都合适。我的建议是别急着画图先把数据结构摸清楚后面每一步都能省时间。我会从读文件开始一直走到统计和可视化最后给一个能直接复用的分组画像脚本。2. 读入与结构盘点三份 CSV 如何拼成一张分析表2.1 先看文件长什么样再决定读法CSV 数据集的通病是你永远不知道第一行是不是表头、有没有引号转义、有没有空行。我拿到文件后的第一个动作不是pd.read_csv一把梭而是先看文件前几行和大小。常见做法是用wc -l看行数用head看前几行确认分隔符和编码。# 查看文件行数含表头行 wc -l *.csv # 查看前3行确认列名和数据格式 head -3 wine_reviews_1.csv这里wc -l统计的行数可能比实际记录数多一行因为表头也算一行。head能直接看到列名如果列名带空格或者引号后面读入时就要指定参数。我遇到过一次某个 CSV 的列名里有括号和空格直接read_csv后列名变成带空格的字符串后续df.rename处理起来非常麻烦所以这一步别省。确认完文件结构后我一般用pandas.read_csv读入但会显式指定encoding、dtype和keep_default_na因为不指定这几个参数后面很容易栽在编码和数据类型的坑上。import pandas as pd # 读取三个CSV文件显式指定编码和列数据类型 df1 pd.read_csv(wine_reviews_1.csv, encodingutf-8, dtype{price: float64}, keep_default_naFalse) df2 pd.read_csv(wine_reviews_2.csv, encodingutf-8, dtype{price: float64}, keep_default_naFalse) df3 pd.read_csv(wine_reviews_3.csv, encodingutf-8, dtype{price: float64}, keep_default_naFalse) # 快速检查每个文件的行数和列名是否一致 print(df1.shape, df2.shape, df3.shape) print(df1.columns.tolist())dtype{price: float64}是因为价格字段可能有小数默认推断成 int 会丢精度。keep_default_naFalse避免了空字符串被当成 NaN这个在后面我会专门讲。三个文件结构一致是合并的前提先打印列名对比一下如果有差异合并前要做列对齐这一步能省掉后面大量的排错时间。2.2 合并前必须做的三件事列对齐、去重、类型统一三份 CSV 合并不是简单的pd.concat因为每一份文件对应的可能是不同批次的导出列顺序、字段名、个别字段的格式都可能存在细微差异。我一般会先统一列名再统一数据类型最后做行级去重——按酒庄、酒名、年份和评论文本的组合去重。如果只按酒名去重同一个酒庄的同名酒款会被误删。# 统一列名避免大小写和空格差异 standard_cols { Country: country, Region: region, Winery: winery, Wine: wine_name, Price: price, Points: points, Description: description, } for df in [df1, df2, df3]: df.rename(columnsstandard_cols, inplaceTrue) # 纵向合并 df pd.concat([df1, df2, df3], ignore_indexTrue) # 按核心字段去重 df df.drop_duplicates(subset[winery, wine_name, points, description]) print(f合并后总记录数: {len(df)}) print(f去重后记录数: {len(df.drop_duplicates())})rename是在原 DataFrame 上做修改inplaceTrue这里用是安全的因为这三个来源表后续不再单独使用。去重的subset我选择了四个字段而不是全部字段原因是price和region可能有合法空值如果把它们加入去重依据两条完全相同的记录会因为一条缺价格而保留下来造成误判。合并之后打印记录数能直接看出三份文件里有多少重复。2.3 字段语义说清楚每列能干什么、不能干什么这个数据集的核心字段我拆成三类身份字段、数值字段、文本字段。身份字段包括country、region、winery、wine_name用于定位一条评论描述的是哪款酒。数值字段包括price和points其中points是评论者给的评分一般落在 80 到 100 分之间price是当时的市场参考价单位是美元。文本字段是description长度从几十个词到几百个词不等适合做词频统计和简单情感分析。这里有一个容易误用的点points不是百分制成绩它是类似于评委评分的绝对分数。不同酒庄、不同年份之间的points可以直接比较但比较之前要看分布因为大部分酒的分数集中在 84 到 92 分这个区间极端低分和高分都很少。如果你要做高性价比酒款推荐正确的做法不是直接排序取 top N而是在评分分布的长尾里找相对便宜的我在后面会用代码演示。3. 统计分析与可视化准备用评分、价格和产地验证直觉3.1 价格与评分的相关性先算相关系数再分组看拿到合并后的数据第一个值得做的分析是价格是否真的和品质正相关。直觉上贵酒评分高但数据集里可能不是这么回事。反直觉结论常常藏在这种真实数据里中间价位段的酒评分方差最大百元以内的酒反而容易出现 88 分以上的高评分。# 剔除价格和评分为空的记录 df_clean df[(df[price].notna()) (df[points].notna())].copy() # 计算整体相关系数 corr df_clean[price].corr(df_clean[points]) print(f价格与评分的皮尔逊相关系数: {corr:.3f}) # 按价格区间分组看各组评分均值和中位数 bins [0, 20, 50, 100, 500, 5000] labels [20, 20-50, 50-100, 100-500, 500] df_clean[price_group] pd.cut(df_clean[price], binsbins, labelslabels) group_stats df_clean.groupby(price_group, observedTrue)[points].agg([count, mean, median, std]) print(group_stats.round(2))pd.cut的作用是把连续的价格切成区间bins和labels一一对应。observedTrue在 pandas 2.x 里是必须加的参数否则groupby会对空的区间也产生分组。输出结果里如果std标准差在 20-50 美元这个区间最大说明这个价位的酒品质跨度大挑酒需要看具体评论而不是只看价位。这时候相关系数可能只有 0.2 左右属于弱相关——这就是贵酒不一定好的数据证据。3.2 产地和评分的关系用分组聚合找出高产区产地分析是葡萄酒数据集里最常见的切片方式。国家字段直接可用但地区字段要小心——同一个地区在不同国家可能重名比如 California 在美国Rioja 在西班牙。正确的做法是先按国家分组再在国家内部按地区分组而不是直接按地区分组。# 按国家和地区两级分组统计记录数、平均分和平均价格 region_stats ( df_clean.groupby([country, region], observedTrue) .agg( review_count(description, count), avg_points(points, mean), avg_price(price, mean), ) .reset_index() ) # 筛选评论数超过100条的地区避免小样本噪音 region_stats region_stats[region_stats[review_count] 100] # 按平均分排序输出Top 15 top_regions region_stats.sort_values(avg_points, ascendingFalse).head(15) print(top_regions.to_string(indexFalse))agg里我用了三个不同的聚合方式description计数代表评论条数points求均值代表产区平均品质price求均值代表产区价格水平。reset_index把分组键变回普通列方便后续筛选。筛选review_count 100这一条非常关键如果不做一个只有两条评论的小产区可能因为分数高而排到前面造成误导。3.3 可视化前的数据塑形把描述文本切成词频表评论文本是最有信息量但也最需要预处理的部分。直接对整段评论做词频统计没有意义因为冠词、介词会占据高频位置。我一般会做三步全部转小写、去停用词、按词根合并。这里的词根合并不做完整形态还原只做简单的单复数处理够用就行。import re from collections import Counter # 定义一个简易分词函数 def tokenize(text): # 只保留字母字符转小写按空格切分 words re.findall(r[a-z], text.lower()) # 停用词集合按需扩展 stop_words {the, a, an, and, or, but, of, for, with, wine} return [w for w in words if w not in stop_words and len(w) 3] # 对所有评论文本进行分词 all_words Counter() for desc in df_clean[description].fillna(): all_words.update(tokenize(desc)) # 输出出现频率最高的20个词 print(all_words.most_common(20))re.findall(r[a-z], text.lower())这一步把文本里所有连续的字母序列抽出来标点符号和数字被直接丢弃。停用词集合里我放了一个 wine 是因为它出现频率极高但对区分葡萄酒风味没有帮助。Counter更新时是逐条评论叠加这里的fillna()是为了防止空值导致attributeerror。4. 避坑清单字段缺失、数据对齐与空值处理的实战记录4.1 现象合并后总行数比预期少了几千行合并三个文件后我用len(df)对比了三份文件行数之和发现少了约 2000 行。第一反应是去重drop_duplicates删多了。检查后发现问题是pd.concat默认按列名对齐——三份文件里有一份的列名是Wine Name带空格另外两份是Wine导致concat后产生了两个不同的列合并后数据没有丢失但drop_duplicates因为列名不一致而把所有行判定为唯一直接删掉了重复行。原因三份 CSV 来自不同批次的导出列名不统一是常态。解决合并前先df.columns.str.strip().str.lower()做统一归一化再rename到标准列名。从那以后我每拿到一批 CSV第一件事就是打印列名做对齐绝不跳过。4.2 现象价格列的均值高达 800 美元明显失真第一次做分组统计时我发现某些地区的平均价格高得离谱检查原始数据后发现有几十行价格是 999 或 888 这种整数明显是缺价的占位符不是真实价格。原因数据导出时缺失价格可能被填充成了特定标记值而read_csv默认把它们读成正常数字。# 将异常价格标记值替换为缺失值 import numpy as np df_clean[price] df_clean[price].replace([888, 999, 0], np.nan) # 重新统计查看缺失比例 missing_rate df_clean[price].isna().mean() print(f价格缺失率: {missing_rate:.2%})处理原则是先看price的取值分布确认哪些是标记值再决定替换还是删除。replace方法支持传入列表一次替换多个值。替换成np.nan后后续分析里用dropna统一过滤。0也包含在替换列表里因为实际在售的葡萄酒不可能标价 0 美元。4.3 现象评分列出现 99 分和 100 分但对应评论内容有明显褒贬不一致检查高分记录时发现一些 99 分的酒在评论里用了 poor finish 或 disappointing 这样的负面词。原因评分的列可能有错位——导出时某行数据结构发生偏移评分字段读到了相邻列的位置。解决这类问题没有捷径只能是抽样交叉验证对每个点位字段做范围检查points必须在 80 到 100 之间price必须大于 0description必须有超过 20 个字符。# 范围合理性检查 invalid_points df_clean[(df_clean[points] 80) | (df_clean[points] 100)] invalid_price df_clean[df_clean[price] 0] invalid_desc df_clean[df_clean[description].str.len() 20] print(f越界评分: {len(invalid_points)} 条) print(f非法价格: {len(invalid_price)} 条) print(f异常短评论: {len(invalid_desc)} 条)这一步的价值不只是清洗更重要的是确认数据整体可信度。越界评分和非法价格可以用drop或replace处理但异常短评论要慎重——有可能是正常评论只是特别简短。实操里我建议把筛选条件放宽到 10 个字符以下再做人工确认避免误删。4.4 现象同一款酒出现两条完全相同的评论但去重后仍然存在重复记录的去重我在前面已经做了但做完全字段去重后仍有少量重复。排查后发现是description字段里包含不可见的换行符差异比如一条是\n结尾另外一条是\r\n结尾。原因CSV 导出时如果经过不同的操作系统换行符会被转成不同形式。解决去重前先做文本规范化把\r\n统一替换为\n并做 strip。# 规范化描述文本去除换行符差异 df_clean[description_norm] ( df_clean[description] .astype(str) .str.replace(\r\n, \n, regexFalse) .str.strip() ) # 基于规范化后的描述去重 df_dedup df_clean.drop_duplicates(subset[winery, wine_name, description_norm]) print(f规范化去重后: {len(df_dedup)} 条)astype(str)是防止个别行是 NaN 导致后续str.replace报错。我用正则regexFalse做了字面量替换因为这里不需要正则表达式直接替换更安全。去重后description_norm可以保留后续做词频分析时直接用它而不是原始字段。5. 进阶技巧用分组聚合快速生成酒款画像表5.1 画像表是什么为什么有用酒款画像表是我自己习惯的一个叫法本质上是一张把「风格关键词、价位段、评分、产地」压缩到一行的汇总表。传统做法是写完分析后逐个找典型酒款费时间。我一般会先做一张画像表把每个葡萄酒品种对应的最高频风味词、平均评分、平均价格算好再去原文里挑具体酒款就非常快。这张表对写推荐文案或者做快速筛选都很有用。5.2 生成画像表的具体步骤# 提取品种名称从酒名中识别常见品种关键词 varietal_keywords { cabernet sauvignon: [cabernet, cab], chardonnay: [chardonnay], pinot noir: [pinot], merlot: [merlot], syrah: [syrah, shiraz], sauvignon blanc: [sauvignon], } def detect_varietal(wine_name): if not isinstance(wine_name, str): return unknown wine_lower wine_name.lower() for varietal, keywords in varietal_keywords.items(): for kw in keywords: if kw in wine_lower: return varietal return unknown df_dedup[varietal] df_dedup[wine_name].map(detect_varietal) # 按品种分组聚合价格、评分和评论关键词 def get_top_keywords(series, top_n5): counter Counter() for text in series.fillna(): counter.update(tokenize(str(text))) return , .join([word for word, _ in counter.most_common(top_n)]) varietal_profile ( df_dedup.groupby(varietal, observedTrue) .agg( count(varietal, size), avg_points(points, mean), avg_price(price, mean), top_keywords(description_norm, get_top_keywords), ) .reset_index() ) # 筛选出现次数超过50次的品种避免小样本 varietal_profile varietal_profile[varietal_profile[count] 50] print(varietal_profile.round(2).to_string(indexFalse))detect_varietal函数用最简单的子串匹配识别品种没有引入复杂的命名实体识别对这份数据来说够用了。map方法把函数应用到wine_name的每个值上。get_top_keywords是一个聚合函数传入的是 Series返回的是字符串所以可以传给agg。这种自定义聚合方式和count、mean混用是没有问题的但要注意get_top_keywords必须接收一个 Series 并返回一个标量否则agg会报错。5.3 画像表怎么用筛选高性价比酒款有了画像表筛选逻辑就清晰了先看每个品种的平均评分和平均价格找出「评分高于该品种均值、价格低于该品种均值」的酒款。这样做的依据是——同一品种内部的评分方差通常小于不同品种之间的方差。直接跨品种比分数没有意义但品种内部比就有参考价值。# 计算每个品种的评分均值并与单瓶酒对比 merged df_dedup.merge( varietal_profile[[varietal, avg_points]], onvarietal, howleft, ) # 筛选高于品种均值、价格低于100美元的酒款 high_value merged[ (merged[points] merged[avg_points]) (merged[price].notna()) (merged[price] 100) ] # 按评分降序输出最值得关注的10款 result ( high_value.sort_values(points, ascendingFalse) .head(10)[[varietal, wine_name, points, price, region]] ) print(result.to_string(indexFalse))merge是在varietal键上做左连接把品种均值拼到每一行。筛选条件同时要求points avg_points和price 100这个组合在真实数据里能筛出大概几百条记录说明数据集里确实存在被低估的酒款——评分高于同类平均水平但价格不到 100 美元。逻辑上看price为空的行被notna()排除了避免低价筛选把缺价记录误判为便宜货。从那以后我把这套流程固定为三个步骤先read_csv时显式声明类型再concat前统一列名最后任何筛选动作前先看一眼isna()统计。每次换一份新数据集我都强制走一遍这个路径。这份葡萄酒评论数据集值得折腾的地方不在于它大而在于它真实地暴露了 CSV 数据在实战中会遇到的各种脏情况希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →