130k葡萄酒评论数据集CSV:从文本清洗到评分预测全流程

发布时间:2026/10/7 5:34:23
130k葡萄酒评论数据集CSV:从文本清洗到评分预测全流程
简介葡萄酒评论数据集是一份面向数据挖掘、机器学习和文本分析场景的公开语料共含三个文件整体压缩包约26.84MB以CSV和JSON两种主流格式组织。数据汇集13万余条真实品鉴记录字段覆盖品种、产地位置、酒庄名称、价格以及评论描述既能支撑价格预测回归任务也可用于产区和酒庄的聚类对比、评论文本的语义分析或关键词提取。两个CSV文件分别包含13万行和15万行结构化记录列式规整便于直接读入Pandas或Spark进行特征工程JSON文件亦提供近7000个嵌套评论节点适合练习JSON解析或作为文档型数据库的导入样例。整套数据字段语义明确、体量适中从入门级图表展示到进阶模型训练皆可适用。资源目前已有102人学习下载对于想快速获得干净、真实葡萄酒评论数据的研究者或开发者而言是一份可直接取用的参考集。1. 葡萄酒评论数据集CSV为什么一套评语文本能同时喂给NLP和数据挖掘130k条葡萄酒评论记录被拆成三个CSV文件这事听起来像普通的归档整理但对做数据分析的人来说它其实是一份难得的“带主观文本 带结构化评分”的练手数据每条记录里有品酒师写的评语、对应的点数评分、价格、产区、品种和酒庄信息。你既可以拿它练自然语言处理把评语变成预测葡萄酒得分的特征也可以拿它做探索性数据分析看价格和评分到底是不是正相关。适合的人群很直接刚学 pandas 想找一份真实数据练清洗的人、做文本特征工程缺标注数据的人、以及想找个中等规模数据集跑通分类或回归全流程的算法工程师。下面这套流程是我处理同类 CSV 数据集时常用的方案参数和坑都按 130k 这个规模来设计。2. 三文件结构先摸清读取路径、编码与字段探查脚本拿到三个 CSV 文件第一反应别急着画图先搞清楚三张表的角色。常见做法是三个文件分别存放主记录和补充维度表比如一个文件存酒款基础信息一个存评论正文和评分一个存酒庄/产区映射。但也存在另一种可能——三个文件是按年份或地区切分的分片合并方式完全不同。所以第一步永远是用脚本把 shape、列名、缺失率、唯一值数量打出来。2.1 不急着分析先把三张CSV的形状与缺失率盘出来import pandas as pd from pathlib import Path data_dir Path(./wine_reviews) files list(data_dir.glob(*.csv)) print(找到文件:, [f.name for f in files]) for f in files: try: df pd.read_csv(f, encodingutf-8) except UnicodeDecodeError: df pd.read_csv(f, encodinglatin1) print(f\n {f.name} ) print(shape:, df.shape) print(列名:, df.columns.tolist()) print(缺失率(前10列):) print((df.isna().mean().sort_values(ascendingFalse).head(10) * 100).round(2))逻辑说明我刻意先尝试 utf-8 再回退 latin1是因为很多葡萄酒数据集在生成时用的并不是标准 utf-8——评论里带重音符号的法语、西班牙语词经常被 Excel 或旧系统存成 latin1。直接指定 utf-8 会在读取阶段抛 UnicodeDecodeError整个流程当场卡死。这个回退写法比手动指定编码要稳但注意它掩盖了编码问题后面第 5 章我会讲怎么定位真实编码。参数说明glob(*.csv)只匹配当前目录下的 CSV不递归子目录如果你的文件在嵌套文件夹里用rglob(*.csv)。df.isna().mean()返回每列缺失比例sort_values(ascendingFalse).head(10)只显示缺失最严重的 10 列避免 20 多列的输出刷屏。这里 130k 行数据用 pandas 默认的 C 引擎读取毫无压力不要为了“大数据”的错觉去开 Dask 或 Spark属于杀鸡用牛刀。2.2 130k行不是大数据但CSV的引号和换行会先给你上眼药跑完上面的脚本你会看到每个文件的行数。记住一个关键点130k 行这个规模瓶颈不在内存而在 CSV 解析器的“脾气”。如果某个文件的行数跟你预期差很多先别怀疑数据损坏大概率是字段值里带了换行符或未转义的引号。import csv with open(./wine_reviews/winemag_reviews.csv, encodingutf-8, newline) as fp: sample list(csv.reader(fp, delimiter,))[:200] # 找出列数不等于表头列数的行 header_cols len(sample[0]) bad_rows [i for i, row in enumerate(sample) if len(row) ! header_cols] print(f前200行中列数异常的行索引: {bad_rows[:10]}) # 看具体某行的原始内容 if bad_rows: idx bad_rows[0] raw open(./wine_reviews/winemag_reviews.csv, encodingutf-8).readlines()[idx] print(原始行片段:, repr(raw[:500]))逻辑说明csv.reader默认处理引号包裹的字段如果字段内部有换行它会把换行当作字段内容而不是行结束符这时len(row)会正常等于表头列数。反过来如果用readlines()按物理行读再按逗号split就会把一条评论拆成两行行数虚增。我见过有人因为这个问题直接把 130k 数据误判成 160k 去汇报属于典型的 CSV 翻车现场。参数说明newline是 Python 官方文档推荐的标准写法避免 Windows 下\r\n被再次翻译delimiter,对标准 CSV 是默认值但如果你遇到的是分号分隔的欧洲版数据这里要改成delimiter;。这段代码的作用不是修复数据而是帮你确认文件本身的物理行数和逻辑记录数是否一致这决定了后续能不能放心用 pandas 一把梭。3. 把评论字段洗干净换行、引号与多表合并的三个硬规则探查清楚三张表的结构后进入真正的体力活清洗。葡萄酒评论数据集最脏的地方集中在两个区域——评论文本字段里混入的转义符号和空白以及三张表关联时主键不唯一导致的合并爆炸。这两块处理不好后续特征工程和建模全是幻觉。3.1 评论文本清洗去掉转义符、HTML实体与连续空白import pandas as pd import html import re def clean_text(s: str) - str: if pd.isna(s): return # 1. 还原常见HTML实体 s html.unescape(str(s)) # 2. 去除换行/回车/tab等控制字符统一成空格 s re.sub(r[\r\n\t\x00-\x1f], , s) # 3. 将连续空白压成单个空格 s re.sub(r\s{2,}, , s) # 4. 去首尾空格 return s.strip() df[description_clean] df[description].apply(clean_text) df[desc_len] df[description_clean].str.len() df[desc_word_count] df[description_clean].str.split().str.len()逻辑说明html.unescape处理amp;和#39;这类实体在爬虫抓取的数据里特别常见Wine 类数据如果经历过网页转存几乎必中。re.sub(r[\r\n\t\x00-\x1f], , s)把 ASCII 控制字符全部转成空格避免后面做词频统计时出现孤立换行符把分词器搞崩溃。最后生成两个新特征字符长度和单词数。这两个特征在后面的评分预测里非常有解释力——很多品酒师的评分习惯是“描述越具体分数越高”词数本身就能当一个弱特征。参数说明pd.isna(s)保证了评论为空时返回空字符串而不是 None否则.apply会报错。\x00-\x1f覆盖了从 NUL 到 US 的所有控制字符比单独写\n\r\t干净得多。\s{2,}用贪婪匹配把两个及以上空白压缩为一个这一步做完文本才适合进入 TF-IDF 或词向量阶段——否则同一个词因为前后空格不同会被分出多个词形。3.2 按酒庄/产区做表连接先查唯一性再 merge顺序不能错# 假设 df_main 是主表, df_winery 是酒庄维表 # 第一步检查维表主键是否唯一 print(winery列重复值数量:, df_winery[winery].duplicated().sum()) # 如果不唯一看重复的是哪些 if df_winery[winery].duplicated().sum() 0: dup_names df_winery.loc[df_winery[winery].duplicated(), winery].unique() print(重复酒庄示例:, dup_names[:5]) # 常见方案用 酒庄产区 作为复合键 print(复合键重复数:, df_winery.duplicated(subset[winery, province]).sum()) # 合并主表左连接维表 df_merged df_main.merge( df_winery, on[winery, province], howleft, validatemany_to_one ) print(合并后行数:, len(df_merged), 原主表行数:, len(df_main))逻辑说明这是三个文件合并时最容易翻车的环节。如果维表的winery列根本不唯一直接df_main.merge(df_winery, onwinery)会产生笛卡尔积——一行评论对应多条酒庄信息130k 行直接膨胀到 200k而你根本察觉不到错误因为 merge 不会报错。所以我在合并且前强制打印duplicated().sum()。如果单键不唯一一个常用补救方案是引入第二列组成复合键比如酒庄加省份。参数说明validatemany_to_one不是摆设它让 pandas 在合并前检查右表键的唯一性。如果右表有重复直接抛MergeError相当于给合并操作上了保险丝。howleft保证主表行数不丢失注释里len(df_merged)和len(df_main)必须相等不相等说明左表自身主键有重复要去查主表是不是同一瓶酒被录了两遍。4. 特征工程到第一版模型从文本到评分预测的完整闭环清洗完成、表也合并好了现在数据里有干净的评论文本、价格、产区、品种、酒庄以及我们要预测的目标——points评分。这一段我按“特征怎么造 → 模型怎么训 → 参数怎么调”拆开讲每步都给出能直接跑的结果。4.1 把文本变特征词数、情感分与酒款复杂度from sklearn.feature_extraction.text import TfidfVectorizer import numpy as np # 基于清洗后的文本做TF-IDF限制特征数量防止维度爆炸 vec TfidfVectorizer( max_features3000, ngram_range(1, 2), stop_wordsenglish, min_df5, max_df0.8 ) tfidf vec.fit_transform(df[description_clean]) print(TF-IDF矩阵形状:, tfidf.shape) # 简单的“复杂度”特征描述里提到的葡萄品种个数字符串匹配 grape_list [cabernet, chardonnay, pinot, merlot, syrah, riesling] df[grape_count] df[description_clean].str.lower().apply( lambda x: sum(1 for g in grape_list if g in x) ) # 文本长度与价格组合成比率特征注意price为0或空时的处理 df[price_safe] df[price].replace(0, np.nan) df[desc_len_per_price] df[desc_len] / df[price_safe]逻辑说明TF-IDF 把每条评语变成 3000 维的稀疏向量ngram_range(1,2)让模型能看到“black fruit”这种双词组合对葡萄酒评论这种形容词密集的文本提升明显。min_df5过滤掉只在少数几条记录里出现的生僻词max_df0.8过滤掉超过 80% 文档都出现的词比如 “wine” 本身这两刀砍完特征量会下降不少模型也更稳。grape_count是一个不需要训练的特征描述里提到的品种越多通常代表这款酒越复杂——这是我常用的一个手动特征。参数说明TF-IDF 矩阵是稀疏的千万不要toarray()转成稠密130k 行 × 3000 维稠密矩阵是 390M 个浮点数内存直接爆。price_safe用replace(0, np.nan)是因为 CSV 里价格可能是 0 或空字符串先转成 NaN后续模型会自动跳过缺失样本。4.2 训练集/验证集切分与基线模型的三个必调参数from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error from sklearn.pipeline import make_union from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.compose import ColumnTransformer # 评分目标先转成数值并丢弃异常值 df[points] pd.to_numeric(df[points], errorscoerce) df_model df.dropna(subset[points]).copy() # 特征集合文本 数值特征 feature_cols [price_safe, desc_len, desc_word_count, grape_count] X_numeric df_model[feature_cols].fillna(-1) # 缺失用-1填充树模型可容忍 X_train_text, X_test_text, y_train, y_test train_test_split( df_model[description_clean], df_model[points], test_size0.2, random_state42 ) # 简化版只用数值特征做基线 X_train_num, X_test_num train_test_split( X_numeric, test_size0.2, random_state42 ) rf RandomForestRegressor( n_estimators300, max_depth20, min_samples_leaf3, n_jobs-1, random_state42 ) rf.fit(X_train_num, y_train) y_pred rf.predict(X_test_num) print(基线MAE:, mean_absolute_error(y_test, y_pred))逻辑说明pd.to_numeric(errorscoerce)把points里像 “90-91” 或空字符串这种脏值转成 NaN再dropna丢掉。我没有直接用原始 X 做训练而是先临时切分文本和数值是因为如果你先填充缺失再切分验证集里会出现训练集统计信息的影子——虽然树模型不受填充值影响但为了养成好习惯一切特征工程都应在切分后再做。fillna(-1)是树模型友好的缺失编码方式千万别给随机森林做均值填充它自己就能处理分裂时的不纯度。参数说明三个必调参数。n_estimators从 100 到 300 提升明显超过 300 边际收益递减训练时间却线性上涨。max_depth建议开在 15-25没有它随机森林每棵树会无限生长130k 数据上很容易过拟合。min_samples_leaf3强制每个叶子至少 3 个样本MAE 通常能比默认值降 0.1-0.2 分代价是训练变慢几分钟但值得。n_jobs-1让所有 CPU 核心并行四核机器上速度接近四倍。5. 避坑专栏处理这套CSV数据集最常见的5个翻车点这套数据的坑不是隐藏的而是披着“正常数据”的外衣潜伏着。下面 5 条全是我处理类似评论文本数据时的血泪经验每条按现象、原因、解决的顺序写可以直接对照排查。5.1 读取时所有重音字符变成乱码现象打开 CSV 或 pandas 读入后café变成café法语产区名全是乱码。原因文件实际是 UTF-8 编码但被某些工具以 latin1 解读后存成了双字节乱码或者反过来——文件是 latin1你以 utf-8 读取抛异常。解决不要用我第 2 章的“回退读取法”蒙混过关用chardet或charset_normalizer检测真实编码再统一转成 utf-8 输出。一步到位的写法是from charset_normalizer import from_path best from_path(./wine_reviews/winemag_reviews.csv).best() df pd.read_csv(./wine_reviews/winemag_reviews.csv, encodingbest.encoding)5.2 评论字段里的换行导致 merge 后行数对不上现象三个文件单独看行数都正常一旦 merge行数要么暴增要么骤减且duplicated()检查主键并没有重复。原因CSV 中 description 字段内包含换行符如果之前有人用 Excel 另存或 Python 的错误方式读取后再导出会把一条记录拆成两条反过来某些行被吃掉。解决永远用csv.reader或 pandas 的 C 引擎读取不要用open().readlines()按行分割再处理导出时统一加lineterminator\n和quotingcsv.QUOTE_ALL确保写入时带引号的字段被正确包裹。5.3 points 字段读取后类型是 object 而不是 int现象df[points].dtype返回object平均值算不了绘图也报错。原因字段里混入了类似90-91、N/A、90这类非纯数字内容只要有一个脏值pandas 就把整列降级为字符串。解决先看脏值长什么样再决定策略import pandas as pd bad_mask df[points].astype(str).str.contains(r[^0-9.], regexTrue, naFalse) print(df.loc[bad_mask, points].value_counts())如果脏值是区间90-91我一般取均值 90.5 或用下界如果是N/A直接dropna。千万别盲目pd.to_numeric(errorscoerce)否则脏值会悄悄变成 NaN你以为是清洗了其实是把数据丢了一半。5.4 用描述文本做特征时验证集MAE极低但真实场景完全失效现象随机森林在测试集上 MAE 只有 1.5 分但拿新的葡萄酒评论去预测误差直接翻倍。原因数据泄漏——description里可能包含了价格、产区、酒庄等可被模型直接“抄作业”的字段或者更隐蔽的你在切分前对整个数据集做了 TF-IDF 拟合验证集已经参与了词表构建。解决用Pipeline把 TF-IDF 和模型包在一起确保fit只在训练集上执行from sklearn.pipeline import Pipeline from sklearn.ensemble import RandomForestRegressor from sklearn.feature_extraction.text import TfidfVectorizer pipe Pipeline([ (tfidf, TfidfVectorizer(max_features3000, ngram_range(1,2))), (rf, RandomForestRegressor(n_estimators300, max_depth20, n_jobs-1)) ]) pipe.fit(X_train_text, y_train)5.5 三文件按年份切分时年份列重复且主键失效现象三个文件分别覆盖 2015、2016、2017 年的评论但合并后同一瓶酒出现多条记录winery province复合键也压不住。原因这种分片文件的真正主键可能是winery province vintage年份你只用了前两者。解决合并前打印三个文件各自的列集合找出真正能唯一标识记录的列组合。如果单一维度都无法唯一用df.groupby(cols).size().sort_values()检查最大分组数量分组大小等于 1 的组合才是合法主键。6. 验证与进阶用留存挑战集检验你的葡萄酒评分预测模型训完不是终点我要额外留一份模型从未见过的数据做“挑战集”——做法是切分时单独切出 10% 样本连同清洗和特征工程一起封存在 pickle 里只有最终评估时才解开。这样能把数据泄漏的风险降到最低验证结果也更有说服力。具体验证指标除了前面用的 MAE我还会看两个东西预测误差在 88-92 分区间内的分布以及模型的偏差方向。import pickle import numpy as np from sklearn.metrics import mean_absolute_error # 解开封存挑战集 with open(holdout.pkl, rb) as fp: X_hold, y_hold pickle.load(fp) # 加载训练好的pipeline with open(final_model.pkl, rb) as fp: final_pipe pickle.load(fp) pred final_pipe.predict(X_hold) mae mean_absolute_error(y_hold, pred) print(f挑战集MAE: {mae:.3f}) # 检查误差是否有偏正值代表预测偏高 bias np.mean(pred - y_hold) print(f平均偏差: {bias:.3f} 分) # 分段观察低分酒和高分酒哪个预测更准 for lo, hi in [(80, 86), (86, 90), (90, 96)]: mask (y_hold lo) (y_hold hi) seg_mae mean_absolute_error(y_hold[mask], pred[mask]) print(f{lo}-{hi}分区间 MAE: {seg_mae:.2f} (样本量 {mask.sum()}))逻辑说明平均偏差是 0.3 分说明模型系统性地把酒评得偏高常见原因是训练集中高分段样本占比过多模型学会了“往高了猜”的倾向。分段 MAE 能告诉你模型到底在哪个区间失效——如果 90 分以上区间 MAE 突然变大说明稀有高分样本不足这时不要盲目加模型复杂度而是去收集更多高分评论或者用class_weight给极端分数加权。参数说明pickle.dump存模型和挑战集时务必用同版本 Python 和 sklearn 序列化跨版本加载轻则告警重则报ModuleNotFoundError。我习惯把特征列名、清洗函数源码、模型版本号一起打包进一个meta.json这样三个月后回来看还能想起当初是怎么处理的。进阶方向再往下走可以尝试用回归模型输出的残差做二分类判断“一款酒是否值这个价”或者把description用预训练 embedding 代替 TF-IDF看对 MAE 的影响有多大——但基线模型一定要留好方便反复对照。我的个人习惯是拿到任何新的 CSV 数据集第一件事永远是跑第 2 章那个探查脚本把 shape、缺失率、脏值分布打印出来贴在笔记里之后再决定建模路线这套工序帮我避开过很多想当然的弯路希望帮到你。本文还有配套的精品资源点击获取