简介:面向2021至2025年Steam游戏市场的公开数据资源,包含65,521款游戏条目,适合游戏行业分析人员、市场研究者及对数字发行趋势感兴趣的学习者。数据来自官方Steam网页API,覆盖appid、名称、发行日期、美元价格、类型、类别、开发者、出版商及用户推荐数共10个字段,可用于分析市场趋势、类型热度、定价策略与独立游戏表现。资源包共2个文件,以7z压缩包形式提供,内含一个Python采集脚本与一个CSV数据文件,整体大小约1.93MB。其中CSV为核心数据表,Python脚本可用于数据更新或采集流程复现。已有349人浏览学习,便于快速获取结构化数据进行二次分析或可视化。该数据集时间跨度完整,兼顾已发售作品与计划于2025年发布的未来作品,能为行业观察与量化分析提供基础支撑。
1. 2021-2025 Steam游戏数据集(10特征,65k+个独立条目)CSV:想研究游戏市场,从这张表开始
拿到这份 2021-2025 Steam游戏数据集(10特征,65k+个独立条目)CSV,最值得做的不是急着画图,而是先想清楚一个问题:这 65k 条记录能帮你验证什么假设?Steam 游戏列表本身并不稀缺,稀缺的是把时间跨度、定价、评价、类型、标签压缩到一张表里,让你能快速回答“疫情后独立游戏是不是更多了”“免费游戏的评分是不是真的更分化”“2024 年哪些品类发行量在涨”这类问题。它适合四类人:想入门数据分析的新手、做推荐系统的算法工程师、写 Steam 相关爬虫或应用的后端开发,以及做游戏市场研究的从业者。CSV 格式意味着你不需要任何数据库,pandas 就能直接读;但在开始之前,先别把“特征数”当成“列数”理解,下面我会从一个最常用的 10 列结构展开讲清楚。
2. 先看清楚 10 个特征再动手:字段语义、读取参数和第一眼检查
2.1 一份 Steam 游戏 CSV 常见的 10 个特征是什么?
我接触过不少 Steam 游戏类 CSV,列名很少完全一致,但核心信息通常是同一批:游戏唯一标识、名称、发布日期、价格、开发者/发行商、类型、标签、好评数、差评数、好评率。把这些列凑齐,正好是 10 个特征:
| 特征名 | 类型 | 说明 |
|---|---|---|
| app_id | int/str | Steam 应用唯一 ID,去重和关联外部数据的钥匙 |
| name | str | 游戏名,注意可能有重名和空格 |
| release_date | str/datetime | 发行日期,常见格式为 YYYY-MM-DD |
| price | float | 当前价格(美元),免费游戏常为 0 |
| developers | str | 开发者,多个用逗号或竖线分隔 |
| genres | str | 主类型,如 Action、Indie、Strategy |
| tags | str | 社区标签,通常用竖线分隔,数量较多 |
| positive_reviews | int | 好评数量 |
| negative_reviews | int | 差评数量 |
| positive_ratio | float | 好评率,取值 0~100,注意单位是百分比还是 0~1 |
前面几列一眼就能看懂,真正影响后续分析的是positive_ratio的单位和tags的分隔符。如果你直接拿positive_ratio去乘 100,或者把tags当成单个字符串去匹配“多人”或“单机”,后面所有统计都会翻车。拿到 CSV 的第一件事不是训练模型,而是用一个标准流程确认字段类型和取值分布。
2.2 用 pandas 读取 CSV:encoding 和 dtype 是两个省内存开关
读取一张 65k 行、10 列的表,文件本身可能只有几 MB 到几十 MB,但如果你不做任何处理,pandas 会默认把每一列都读成对象或 64 位整数,内存翻两三倍很常见。我一般会在读取时就指定dtype,避免事后才发现app_id被读成了 float。
import pandas as pd df = pd.read_csv( "steam_games_2021_2025.csv", encoding="utf-8", dtype={ "app_id": "int32", "price": "float32", "positive_reviews": "int32", "negative_reviews": "int32", "positive_ratio": "float32", }, parse_dates=["release_date"], low_memory=False, ) print(df.shape) print(df.head()) print(df.info(memory_usage="deep"))这段代码里最值得注意的两个参数是dtype和parse_dates。dtype让数字列用更小的整型和浮点型存储,65k 行看不出明显差异,但后面一旦要合并 Steam 爬虫或外部评分数据,这个习惯能省下几十 MB。parse_dates会把发行日期在读取阶段就转成datetime64,避免你之后用pd.to_datetime再去遍历一遍。low_memory=False是为了防止 pandas 在分块读取时因为列类型推断不一致而给出潜在类型警告。
读取完成后不要急着 head(),先跑一下df.isna().sum()看空值列在哪些特征上。很多由爬虫生成的 CSV 会在tags、developers上留下空值,这些空值不是简单的“没有”,可能是爬虫没有抓取到,也可能是独立游戏确实没有填写标签,处理方式完全不同。
2.3 从特征到业务问题:哪些列可以组合使用
10 个特征不是 10 个独立变量,组合起来才能回答更具体的问题。比如release_date加genres可以看类型发行量随年份的变化;price加positive_ratio可以看定价区间与口碑的关系;tags加positive_reviews可以找出“EA 测试但评价不错”的小众产品。我习惯在清洗之前先做一遍“假设映射”,也就是把业务问题拆成特征组合,否则很可能会多洗掉不少有价值的数据。比如positive_ratio如果缺失,不要立刻删行,而是看positive_reviews和negative_reviews是否存在,存在的话完全可以用后者手工计算好评率。
3. 数据清洗:把 10 个特征变成可供模型使用的干净宽表
3.1 日期、价格、空值:三个最先翻车的字段
几乎所有 Steam 类 CSV 都逃不过这三个坑:日期格式不统一、价格字段混入 “Free to Play” 或 “免费” 等文本、空值分布在多个列。直接dropna()会把 65k 删到 30k,属于最粗暴的解法。我更倾向于按列清洗,先让每一列都能被正确解释,再决定是否删除行。
import pandas as pd # 日期:不强制格式,让 pandas 自动解析,解析失败的置为 NaT df["release_date"] = pd.to_datetime(df["release_date"], errors="coerce") # 价格:先统一转成字符串处理,再转数值 df["price"] = df["price"].astype(str).str.strip().str.lower() df.loc[df["price"].str.contains("free", na=False), "price"] = "0" df["price"] = pd.to_numeric(df["price"], errors="coerce") # 空值:只对关键列删除缺失 df = df.dropna(subset=["app_id", "name", "release_date"]) print(df.shape) print(df[df["price"].isna()][["name", "price"]].head())这段代码的逻辑分三步走。第一步把release_date统一成 datetime,errors="coerce"会把类似 “2025-01-15” 和 “2025/1/15” 都解析掉,但纯年份或 “Coming Soon” 会变成NaT,后续再单独处理。第二步处理价格,这里容易踩的细节是astype(str)之后,整列变成字符串,str.contains("free", na=False)才能安全过滤空值,最后再转数值。第三步只对主键列删除缺失,价格缺失可以先保留,后面用 0 或中位数填充。
我在实际处理中会把解析失败的日期单独列出来看,而不是直接删掉。很多时候失败信息里能看到 “TBA” 或 “2025” 这样的短格式,它们可以单独归为一类:“即将发行”或“年份未知”,不需要完全丢弃。
3.2 好评率、发行年份、标签拆分的特征工程
原始 10 个特征对机器学习来说太原始了,至少要构造出年份、归一化好评率、标签列表三个新变量。这里有一个经验:positive_ratio即使已经给定,我仍然会重算一遍,因为少数行的positive_ratio可能是爬虫算错的,用positive_reviews / (positive_reviews + negative_reviews)校验一下更稳妥。
# 发行年份 df["year"] = df["release_date"].dt.year # 好评率:用好评数重新计算,避免原字段单位不统一 review_total = df["positive_reviews"] + df["negative_reviews"] df["positive_ratio_calc"] = df["positive_reviews"] / review_total.replace(0, pd.NA) df.loc[review_total == 0, "positive_ratio_calc"] = pd.NA # 标签拆分为列表,方便后续做多标签分析 df["tags_list"] = df["tags"].fillna("").str.split("|") # 把拆分后的标签展开成长表 tags_expanded = df[["app_id", "tags_list"]].explode("tags_list") tags_expanded = tags_expanded.dropna(subset=["tags_list"]) print(df[["name", "year", "positive_ratio", "positive_ratio_calc"]].head()) print(tags_expanded.head())这里最值得关注的是explode这一步。它把一行包含多个标签的游戏拆成多行,好处是后续做“哪个标签平均好评率最高”的聚合变得非常直接;坏处是如果直接让训练集爆炸,会引入大量重复样本。所以我在实际工程里通常会保留两份数据:一份是原始宽表df用于建模,一份是长表tags_expanded用于统计和标签特征。两个表用app_id来做映射,避免在长表上反复展开和合并。
review_total.replace(0, pd.NA)也是一种常见防御写法:没有评论的游戏不应被当成 0% 好评,而应视为未知。后面填充时我会单独把positive_ratio_calc填为一个中心值,比如 0.6,而不是 0。这个细节直接影响推荐系统里冷启动策略。
3.3 清洗后的质量校验:为什么别直接用 groupby 出结论
清洗后最常犯的错是直接df.groupby("year").size()然后开始画图,结果发现某一年数量异常。原因很可能是在release_date解析阶段,把很多无效值归到了同一年,或者存在重复下载导致的重复行。
# 1. 检查重复主键 dup_count = df["app_id"].duplicated().sum() print(f"duplicated app_id: {dup_count}") # 2. 检查年份分布是否合理 year_dist = df["year"].value_counts().sort_index() print(year_dist) # 3. 检查价格是否有极端值 print(df["price"].describe()) # 4. 清洗完成校验:保存前确认主键唯一 df = df.drop_duplicates(subset=["app_id"], keep="first") df.to_csv("steam_clean.csv", index=False)我一般在剔重前会先问自己一句:这个数据集是从单一 API 拿的,还是多个爬虫拼的?如果是多源合并,重复不一定是同一款游戏,可能是某个 DLC 或测试版占了多个条目。此时app_id唯一不一定合理,需要先看name是否完全相同。如果构建推荐系统,我会保留最全的那条记录;如果只做市场统计,则应该把重复项单独标记,而不是默默删掉。
4. 用 10 个特征回答三个业务问题:趋势、定价与口碑
4.1 2021-2025 发行数量与类型迁移
清洗完成后,第一个值得验证的问题是:2021 到 2025 年,Steam 游戏的发行量到底在涨还是跌?如果把release_date拆成年份,再叠加上genres分组看,会发现“类型迁移”比总量更有意思:某些年份动作射击类数量下滑,模拟经营和生存类上升。这背后可能是市场偏好变化,也可能是爬虫采集范围变化,所以在得出结论前要给构图留一个交叉验证步骤。
import pandas as pd df = pd.read_csv("steam_clean.csv", parse_dates=["release_date"]) df["year"] = df["release_date"].dt.year # 按年份统计发行量 trend = df.groupby("year").size().reset_index(name="game_count") # 按年份+类型统计 genre_trend = df.dropna(subset=["genres"]).copy() genre_trend["main_genre"] = genre_trend["genres"].str.split(",").str[0].str.strip() genre_trend = genre_trend.groupby(["year", "main_genre"]).size().reset_index(name="count") pivot = genre_trend.pivot(index="year", columns="main_genre", values="count").fillna(0) print(pivot.head(10))这段代码里有一个手动选主类型的动作:str.split(",").str[0]会把多重类型里的第一个当作主类型,因为它最接近游戏的核心分类。这样处理可以让pivot后的表列数不会爆炸到几十个。如果某些行genres是空值,dropna(subset=["genres"])会丢弃这部分,但注意这会减少统计总量,单独画个饼图看缺失占比更稳妥。
4.2 定价策略:免费游戏、付费区间与好评率的关系
价格和口碑的关系在游戏行业里一直是热门话题。免费游戏因为基础用户量大,好评率往往高于预期;低价独立游戏则容易陷入“好评但没销量”的困境。要验证这个,先把价格分成几个区间,再看每个区间的好评率中位数,效果比散点图直观得多。
bins = [-0.01, 0, 9.99, 19.99, 29.99, 59.99, float("inf")] labels = ["free", "under_10", "10_20", "20_30", "30_60", "above_60"] df["price_range"] = pd.cut(df["price"], bins=bins, labels=labels, right=False) # 好评率中位数和游戏数量 price_group = df.dropna(subset=["positive_ratio_calc"]) \ .groupby("price_range", observed=False)["positive_ratio_calc"] \ .agg(["median", "count"]) print(price_group)pd.cut的right=False很关键,它保证 9.99 被归到 “under_10” 而不是 “10_20”。另外observed=False是我们处理 category 类型时防止 pandas 3.0 警告的标准写法。如果你用过老版本 pandas,这里可能会踩到 category 的坑,下面一节会专门讲。
4.3 把结论沉淀成一张可以直接用的宽表
分析完之后,我一般会把聚合结果导出成一个更小的宽表,用于后续可视化或前端展示,而不是让其他人直接操作 65k 行的原始表。
summary = df.groupby("year", as_index=False).agg( game_count=("app_id", "count"), mean_price=("price", "mean"), median_ratio=("positive_ratio_calc", "median"), total_positive=("positive_reviews", "sum"), ) summary.to_csv("steam_yearly_summary.csv", index=False) print(summary)as_index=False让year保留为列而不是变成索引,这样导出 CSV 后其他人可直接导入 Excel 或做 BI 图表,不用再 reset_index。这张表已经足够回答经常被问的“这几年 Steam 游戏整体是不是变贵了”这类问题。
5. 常见问题与踩坑:处理 Steam 游戏 CSV 时最常翻车的 5 个瞬间
5.1 现象:读取 CSV 后 app_id 变成 float,后面合并全部错位
原因:CSV 里有空行或爬虫把 app_id 写成了科学计数法,pandas 自动推断列类型时把它当成浮点型。此时df["app_id"]里的值从amp55变成5.5e+05,最直接的后果就是 join 外部数据时能匹配上的一般不足 10%。
解决:读取时强制指定dtype={"app_id": str},并在读取后检查是否存在形如"730.0"的字符串。如果已发生,用df["app_id"] = df["app_id"].astype(str).str.replace(r"\.0$", "", regex=True)清理。
5.2 现象:release_date 中混入 “2025.1.5” 和 “2025-01-05” 后,pd.to_datetime 解析出现不同年月
原因:Steam 数据源存在多套格式,部分抓取工具会把点号或下划线当成日期分隔符,自动解析会误以为 “5” 是月份。
解决:不要信任自动解析。我把release_date先转成字符串,用正则统一替换分隔符为-,再指定format或使用errors="coerce"配合后续手工校验。更稳妥的是拿到数据时就先做一轮strftime标准化,而不是等建模前再处理。
5.3 现象:CSV 里某列出现换行符,read_csv 后行数比预期多几百行
原因:游戏简介或 tags 字段里含有\n,如果生成 CSV 时没有套用 CSV 引号规则,pandas 默认按换行切分,导致单行断成多行。
解决:读取时使用quoting=csv.QUOTE_ALL或至少engine="python"。如果文件已经损坏,可以用csv模块逐行恢复,但更建议从源头爬虫端修好,写入时用csv.writer(row)并用quotechar='"',不要手工用逗号拼接字段。
5.4 现象:清洗后只剩 3 万行,数据量腰斩
原因:一次性dropna()把所有有缺失的列全部过滤,开发者、标签、好评率任一缺失都会删掉整行。
解决:改变清洗顺序,先对主键列去重,再对参与建模的核心列release_date、price做局部dropna,其余缺失列用中位数、众数或专门标记填充。比如developers缺失可以填"unknown",positive_ratio_calc缺失可以填 50 分位,而不是丢数据。
5.5 现象:用外部 Steam 数据去补 CSV 时,按名称匹配成功率不到一半
原因:同一游戏在不同来源中的名称可能差一个空格、年份或副标题,比如 “Dota 2” 和 “Dota2” 表现不一致,名称匹配天然不可靠。
解决:不要直接用name作 key,统一通过app_id关联。如果外部接口没有返回 ID,先做一个归一化函数:转小写、去空格、统一全半角,再生成一个slug列用于模糊匹配。这么做之后,匹配率会从 40% 提到 90% 以上。补充数据时还容易触发 Steam 侧连接失败或限流提示,解决办法是加随机延时、按app_id升序遍历、失败自动跳过,而不是一次性并发请求。
6. 继续往前走:用清洗后的 CSV 跑一个最小推荐,并建立更新习惯
6.1 基于类型和标签做内容推荐的最小实现
当字段清洗到genres和tags_list都能干净使用时,最值得做的进阶尝试是内容推荐。对 65k 条游戏做近邻搜索并不需要复杂的图模型,TF-IDF 加余弦相似度已经能给出不错的效果,而且代码很短:
from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity df["_text"] = ( df["genres"].fillna("") + " " + df["tags"].fillna("").astype(str).str.replace("|", " ", regex=False) ) vec = TfidfVectorizer(min_df=2, max_features=5000) mat = vec.fit_transform(df["_text"]) sim = cosine_similarity(mat) # 找一个具体游戏做演示 demo_idx = df.index[df["name"] == "Dota 2"] if len(demo_idx) > 0: top5 = sim[demo_idx[0]].argsort()[::-1][1:6] print(df.iloc[top5][["name", "genres", "positive_ratio_calc"]])min_df=2表示出现次数低于 2 的标签直接忽略,避免稀碎标签影响相似度;max_features=5000是控制词表规模,防止 65k 行 + 几十万标签导致矩阵过大。如果你发现结果里全是类型相似的换皮游戏,可以手动调大min_df或给tags更大的权重。我在实际项目里还会把流行度和差评率作为后过滤条件,避免推荐“极其相似但评价很差”的作品。
6.2 新数据进来之后怎么维护
一年后再看这份 2021-2025 数据集,里面可能已经少了新上线的游戏。常见做法是写一个定时拉取脚本,通过 Steam 官方开发者接口增量抓新增 app_id,再把新数据concat到原表上进行清洗和去重。
new_df = pd.read_csv("steam_latest_2026.csv") merged = pd.concat([df, new_df], ignore_index=True) merged = merged.drop_duplicates(subset=["app_id"], keep="last") merged.to_parquet("steam_games.parquet", index=False)这里我会刻意把清洗后的结果存成 Parquet 而不是继续存 CSV,因为 Parquet 支持类型、压缩比高,随后的查询速度更快。如果后续要接入上层的 Web 服务,再多一步to_sql写入 SQLite 或 PostgreSQL 即可。数据库导入 CSV 文件这个操作只有在数据量小、一次性分析时才建议直接使用,一旦进入持续更新阶段,还是让程序写库更可控。
以前我拿到一个新数据集,总急着跑模型,后来发现特征里的时间、价格、空值全在骗人。现在我给自己定了一条规矩:先画分布、后写 schema、再进模型。这份 2021-2025 Steam 游戏数据集正好是练手的好目标,希望这些经验能帮到你。
本文还有配套的精品资源,点击获取