2026/10/11 9:14:23

130k+葡萄酒评论CSV全流程分析:pandas清洗、统计与价格预测实战

130k+葡萄酒评论CSV全流程分析:pandas清洗、统计与价格预测实战 简介一份包含130k条记录的葡萄酒评论数据集覆盖品种、酒庄、产区位置、价格与品鉴描述等核心字段适合数据分析、机器学习或葡萄酒市场研究的初学者与从业者作为真实样本练习。资源共3个文件打包为7z格式整体大小约26.84MB两个CSV文件分别提供130k行和150k行结构化评论便于直接进行统计分析与特征工程一个JSON文件包含约6900个评论节点适合练习嵌套数据解析或关系型转换。目前已有102人学习下载。借助这些数据读者可开展评论情感分析、价格影响因素建模、品种与产区分布统计、评酒师文本风格挖掘等实验同时可对比不同规模CSV数据对模型表现的影响并体会JSON与表格格式在存储和读取上的差异快速积累真实数据集清洗、分析与建模的完整经验。1. 一份 130k 记录的葡萄酒评论 CSV为什么值得你下载并复跑一遍做数据分析的人手里多少都存过几个练手数据集但能同时覆盖导入 csv 文件、类型矫正、缺失值处理、分组统计、建模基线五个环节的数据集其实不多。这份葡萄酒评论数据集一共三份 CSV、13 万条以上评论记录正好把这条链路补齐了字段里有文本品酒笔记、有数值评分、价格、有分类国家、产区、品种、酒庄缺值和重复数据也真实存在不是那种被清洗到毫无脾气的教学样例。适合人群很明确正在练 pandas 的数据分析新人、准备面试前想刷一遍统计口径的从业者以及想快速验证评分到底能不能预测价格这类问题的建模入门者。下面按我实际拆这份数据的顺序讲每一步都给出能直接跑的代码和参数说明。2. 数据集全貌三份 CSV 的分工、字段字典与关联逻辑2.1 拿到压缩包后先别急着分析花两分钟确认文件关系很多人拿到数据集的第一反应是pd.read_csv(第一眼看到的文件)结果读到一半发现字段对不上、行数也对不上。我一般会先做三件事解压后ls -lh看三份文件的大小差异再用head -5看每份文件的表头最后读一次行数。130k 记录分摊到三份文件里常见的组织方式有两种一种是全量主表 去重子表 聚合统计表的组合另一种是按年份或产区切分的分片。两种方式的处理策略完全不同前者要关注主表与子表的记录数差额后者要关心切片之间有没有重叠。import pandas as pd full pd.read_csv(wine_reviews_full.csv, encodingutf-8) sub pd.read_csv(wine_reviews_clean.csv, encodingutf-8) summary pd.read_csv(wine_reviews_summary.csv, encodingutf-8) print(full.shape, sub.shape, summary.shape) print(full.columns.tolist()[:5])逻辑说明shape是最快的体检方式三份文件的行数加起来应该和 130k 这个量级对得上对不上说明有重复或切分时有过滤。columns.tolist()看前几个字段名确认三份文件的表头是否一致。文件实际叫什么名字以解压后为准上面这几个名字只是我常用的命名习惯。提示先确认关联键再决定怎么合并是这条数据链路里最省时间的十分钟。三份文件之间的关联键通常是酒款标题title或者酒庄 品种 年份的组合。如果主表和子表行数差了很多先跑一条full[~full[title].isin(sub[title])]看看被过滤掉的记录长什么样再决定是否需要补回来。这一步是后续所有统计口径的地基地基歪了后面全歪。2.2 核心字段字典与类型规划这份数据集的字段是标准的葡萄酒评论采集结构拿到表头后可以和下面的字典对照确认哪些字段能做数值统计、哪些只适合做分类聚合。字段类型含义使用注意country分类产酒国英文国名聚合前先看 unique 数量description文本品酒笔记适合文本分析注意内含逗号和换行designation分类酒款细分标识缺失比例较高不要作为主分析列points数值品鉴评分通常落在 80–100不是 0–100 满分制price数值价格美元有缺失和极端值建模前要处理province分类省份/州与 country 存在层级关系region_1分类具体产区缺失率低于 region_2region_2分类次产区缺失率可能过半慎用taster_name分类品鉴师部分记录为空可做分布统计title文本酒款标题含年份与酒庄信息注意重复variety分类葡萄品种头部集中长尾多winery分类酒庄与 title 是多对一关系类型规划上我的习惯是points读成可空整数Int64price读成float其余分类字段先用object读进来确认分类数量后再按需转category。13 万行、12 列的数据规模不大但category类型对country、variety这种重复度高的列能省将近一半内存后面做groupby也会更快。description里如果混入了换行符CSV 会用引号包裹整个字段读取时不要手动改分隔参数默认的逗号分隔就能正确处理。2.3 样本平衡性先看国家和品种的分布再动手统计分析最怕的是样本不平衡这份数据恰恰在国家这个维度上极度倾斜。按这类葡萄酒评论数据的常见分布美国产区的评论量会占去将近一半法国、意大利、西班牙跟在其后剩下的几十个国家分摊末尾的长尾。品种维度类似Pinot Noir、Chardonnay、Cabernet Sauvignon 这类主流品种评论数巨大而一些小众品种只有几十条。print(full[country].value_counts().head(10)) print(full[variety].value_counts().head(10)) print(full[country].nunique(), full[variety].nunique())逻辑说明value_counts()直接给出每个类别的记录数nunique()给出类别总数。如果统计目标是各国平均分对比就必须意识到美国的样本量会主导整体均值正确的做法是分组计算时同时输出记录数再决定要不要对长尾国家做过滤或加权。我一般会设定一个最低记录数阈值比如只保留评论量大于 100 的国家进入对比避免两三条评论就把某个小国的平均分拉得很离谱。3. 从 CSV 到可分析的数据框读取、类型矫正与缺失值处理3.1 读取三份 CSV 并核对 shape进入正式分析前先把三份文件都读进来做一次体检。这一步的目的不是跑通代码而是确认文件编码、行数和列数这三个基本事实。CSV 最常见的翻车点就是编码这份数据如果是从英文平台抓取的utf-8通常没问题但如果你在 Windows 上用 Excel 另存过编码可能变成cp1252或gbk读取时直接报UnicodeDecodeError。import pandas as pd full pd.read_csv(wine_reviews_full.csv, encodingutf-8) print(full.shape) print(full.dtypes)参数说明encodingutf-8是用得最多的编码报错就换成encodinglatin1或cp1252不要硬扛。sep默认是逗号这份数据是标准 CSV不需要改。dtypes输出每列的类型这里能看到points是不是已经被推断成了float64——如果是说明列里有空值下一步就要做类型矫正。3.2 类型矫正与内存优化13 万条记录的数据类型矫正的目的有两个一是让数值列真正参与计算二是把重复度高的文本列转成category节省内存。points这个字段很典型原始数据里评分是整数但存在少量空值pandas 会自动推断成float64导致后面做分组均值时出现 87.5 这种奇怪的分数。df full.copy() df[points] pd.to_numeric(df[points], errorscoerce).astype(Int64) df[price] pd.to_numeric(df[price], errorscoerce) cat_cols [country, province, region_1, variety, winery] for col in cat_cols: df[col] df[col].astype(category) print(df.dtypes) print(df.memory_usage(deepTrue).sum() / 1024**2, MB)逻辑说明pd.to_numeric(..., errorscoerce)的作用是遇到无法解析的脏值时不报错而是转成 NaN避免一条坏数据让整个程序中断。.astype(Int64)是可空整数类型既保留了整数语义又允许 NaN 存在。category类型对重复度高的分类列会做内部映射memory_usage(deepTrue)可以直观看到转换前后的内存差异。参数说明errors参数有三个可选值raise是遇到错误直接抛异常coerce是转 NaNignore是保留原值我几乎只用coerce。3.3 缺失值分布与处理策略缺失值处理最忌讳的就是无脑dropna()。先算清楚每列的缺失率再按业务场景决定取舍。这份数据里description和title一般不会缺缺的主要集中在region_2、designation、price这几个字段。missing df.isnull().sum() missing missing[missing 0].sort_values(ascendingFalse) print(missing / len(df))逻辑说明isnull().sum()统计每列空值数量除以总行数得到缺失率。我惯用的策略是分三类处理第一类region_2缺失率如果过半直接弃用或者只在需要该字段的分析里做子集筛选第二类price缺失但其他字段完整按province的中位数填充注意不是均值第三类points缺失的记录极少直接删掉不影响大局。designation缺失率高且业务价值有限保留 NaN 不填充因为大多数统计函数会自动跳过它。3.4 重复记录筛查与去重抓取类数据集几乎必然有重复记录问题是重复的定义。只看title一列去重会误删大量有效记录因为同一个酒庄同一年份可能有多款不同酒它们的title各不相同但同一款酒被多次抓取时title、winery、points、price会完全一致。dup df.duplicated(subset[title, winery, points, price], keepFalse) print(dup.sum()) print(df[dup].head(10))逻辑说明duplicated(subset..., keepFalse)会把所有重复的行都标记出来包括第一次出现的。先看重复样本长什么样确认是不是同一条记录的多次抓取再决定去重方式。如果确认是抓取产生的重复用drop_duplicates(subset[...])保留第一条即可如果重复记录里price有差异那可能是同一款酒不同渠道的价格需要保留并单独分析不能直接删。4. 统计口径评分分布、价格分位与品种偏好怎么算4.1 评分分布先确认量纲再谈均值葡萄酒评分这套体系里points的合理范围是 80 到 10080 以下和 100 以上都属于异常值。拿到数据后第一件事不是算均值而是先看describe()和分布图确认量纲。很多人第一次看到均值 88 就以为是百分制里的 88 分其实在葡萄酒评分体系里 88 属于良好偏上的水平90 分以上才算真正的高分酒。print(df[points].describe()) print(df[points].value_counts().sort_index().head(20))逻辑说明describe()给出 count、mean、std、min、四分位数一眼能看出数据范围和异常情况。value_counts().sort_index()按分数从低到高排列能看出评分聚集在哪个区间。按这类数据集的常见分布评分会聚集在 86 到 92 之间90 分以上的记录占比通常不到 15%这个比例决定了高分酒这个口径在后续分析里的天然稀缺性。4.2 价格分位数与长尾处理价格字段是最典型的右偏分布少数几百上千美元的名庄酒会把均值拉得很高中位数才是更稳健的集中趋势度量。处理价格的第一步是看分位数而不是看均值。import numpy as np price_q df[price].quantile([0.25, 0.5, 0.75, 0.95, 0.99]) print(price_q) df[log_price] np.log1p(df[price]) print(df[log_price].describe())参数说明quantile([0.25, 0.5, 0.75, 0.95, 0.99])的传入列表就是你想看的分位点业务上关心哪段就保留哪段。np.log1p是log(1x)的数值稳定写法好处是price0时不会产生负无穷。逻辑说明如果 99 分位的价格是 200 美元而最大值到几千美元说明极端值只影响极少数样本后续建模时用log_price做目标变量预测完再用expm1还原成美元这个习惯能显著缓解模型对长尾的敏感。4.3 国家与品种的交叉统计分组统计是这份数据最出活的部分。按国家聚合看平均分和中位价再按品种聚合看评论热度和价格档位基本就能勾勒出这份数据集的业务概貌。这里的关键参数是聚合口径均值、中位数、记录数各有用处必须同时输出。top_countries df[country].value_counts().head(10).index agg_country df[df[country].isin(top_countries)].groupby(country).agg( 平均分(points, mean), 中位价(price, median), 记录数(title, count) ).sort_values(记录数, ascendingFalse) print(agg_country)逻辑说明先用value_counts().head(10)取出评论量前十的国家再对子集做groupby聚合避免长尾国家干扰表格可读性。agg里用(points, mean)这种元组写法是 pandas 2.x 推荐的方式不用rename也能给结果列起中文名。用中位价而不用均价是因为价格长尾会让均值失真。按这类数据的常见结果法国的中位价会明显高于美国和西班牙但评论量少一个量级——这就是样本不平衡在实际统计里的直接体现。4.4 评分与价格的相关性log 变换前后的差异评分和价格的关系是这份数据最容易被过度解读的地方。直接算points和price的皮尔逊相关系数通常在 0.3 到 0.4 之间属于弱正相关但对price做对数变换后相关系数会明显上升。这个差异是了解统计口径的人才会注意到的细节。corr_raw df[[points, price]].corr().iloc[0, 1] corr_log df[[points, log_price]].corr().iloc[0, 1] print(corr_raw, corr_log)逻辑说明iloc[0, 1]取相关系数矩阵第一行第二列的值即 points 与 price 的相关系数。原始价格的长尾会让散点图挤成一团相关性被压缩取对数后散点图拉开线性关系更明显。从业务角度解释价格里很大一部分由品种、产区、酒庄声誉决定评分只贡献其中一小部分——这个结论对后面建模时特征选择很有指导意义不用指望只靠评分就能精准预测价格。5. 避坑手记130k 记录里最容易栽的五个坑以下五条都是处理这类评论数据集时实打实遇到过的翻车现场每条按现象 → 原因 → 解决梳理你可以直接对照自己跑到哪一步中了招。5.1 points 列读进来全是浮点型分组均值出现 87.5现象df[points].dtype显示float64按国家分组后平均分出现 87.5、88.7 这类小数。原因原始 CSV 的 points 列里存在少量空值pandas 在推断类型时为了保证不丢数据自动把整列升级成浮点型。解决读取后立即执行pd.to_numeric(df[points], errorscoerce).astype(Int64)把空值单独保留整数语义归位或者在read_csv时直接指定dtype{points: Int64}。5.2 region_2 缺失过半无脑 dropna 把样本砍掉一半现象执行dropna()后行数从 13 万直接掉到 6 万左右后续分组统计全部失真。原因region_2只在部分旧世界产区的酒款里有记录新世界产区这一列大面积缺失它不是数据质量问题而是业务上本来就没有。解决先算缺失率再决策。只有真正需要次产区维度的分析才做子集筛选其他场景要么填 Unknown 保持行数要么直接不读这一列。数据清洗的目的是保留有效信息不是为了把所有列都填满。5.3 直接信任聚合文件和主表自算结果对不上现象用第三份聚合文件里的国家平均分排序和自己在主表上groupby算出来的结果有明显出入。原因聚合文件可能做过去重、过滤空值或者只统计了部分时段的数据统计口径与主表不一致。解决拿到聚合文件后先复算一遍再使用。对比聚合文件的记录数和主表的记录数找出差额来源实在对不上就以主表为准聚合文件只做交叉验证。这也是为什么我一直强调分析顺序先主表、后子表、再看聚合。5.4 单列 title 去重误删大量有效记录现象df.duplicated(subset[title]).sum()显示有几千条重复去重后一些知名酒庄的评论量骤减。原因同一酒庄同一品种不同年份的酒款title各不相同只看title会把同酒不同年份和同酒重复抓取混为一谈。解决用[title, winery, points, price]组合去重只删掉完全一模一样的抓取副本。如果四条记录里只有price不同说明是不同渠道的价格样本保留并单独分析更有价值。5.5 price 里混着 0 和上千美元的极端值直接建模把评估指标拉爆现象用原始price训练回归模型MAE 高达几百美元特征加了再多也降不下来。原因price0是录入错误或缺失的占位符上千美元的名庄酒是真实存在的长尾回归模型对这两类样本都极其敏感。解决训练前先看describe()和分位数对price取log1p变换后再建模预测结果用expm1还原price0的记录单独处理要么剔除要么标记。这个习惯能让 MAE 回到合理量级模型评估才有参考意义。6. 进阶用评分和品种热度跑一版价格预测基线验证数据的信息含量6.1 特征构造与训练集划分数据清洗做完可以做一个最小可行的价格预测模型目的不是追求精度而是验证这份数据里到底有多少可解释的信息。特征只取两个评分points和品种热度variety_freq后者用品种在数据里的出现频数做编码相当于用评论量代表品种的市场热度。from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error model_df df.dropna(subset[price, points]).copy() model_df[log_price] np.log1p(model_df[price]) freq model_df[variety].map(model_df[variety].value_counts()) model_df[variety_freq] freq model_df[is_high] (model_df[points] 90).astype(int) features model_df[[points, variety_freq, is_high]] y model_df[log_price] X_train, X_test, y_train, y_test train_test_split( features, y, test_size0.2, random_state42 ) rf RandomForestRegressor(n_estimators200, max_depth10, n_jobs-1, random_state42) rf.fit(X_train, y_train) pred rf.predict(X_test) print(mean_absolute_error(y_test, pred))参数说明test_size0.2是常规划分比例random_state42固定随机种子保证每次运行结果一致n_estimators200和max_depth10在这个数据规模下几分钟内能跑完n_jobs-1让模型使用全部 CPU 核心。逻辑说明variety_freq用出现频数做编码是分类特征处理里最省事但有效的方法is_high把是否 90 分以上这个业务口径显式变成特征能单独评估高分标签的解释力。6.2 结果边界与验证习惯按这类数据的常见结果在 log 空间里 MAE 大约在 0.4 到 0.6 之间还原成美元大约是 8 到 12 美元。这个精度不算好但它的价值在于对照先跑一条只有points的回归做底线再加variety_freq再加is_high每加一个特征看 MAE 下降多少就能清楚知道每个维度的信息贡献。你会发现品种热度带来的提升远大于评分——这恰好印证了业务直觉价格由品种、产区、酒庄声誉决定评分只是一个结果指标。从那以后我每拿到一份 CSV 数据都会强制自己先走一遍df.info()、shape、isnull().sum()和duplicated这四个体检项再进入任何可视化或建模。很多翻车其实都发生在第一行代码之前这四个检查项就是我的后悔药。希望帮到你。本文还有配套的精品资源点击获取