阿迪达斯数据项目实战:从Python数据采集到销量预测

发布时间:2026/10/14 4:29:08
阿迪达斯数据项目实战:从Python数据采集到销量预测
简介这是一个由React官方脚手架生成的TypeScript项目模板面向需要快速搭建前端应用的前端开发者能帮助理解从开发调试到生产构建的完整流程并解决项目初始化和配置管理繁琐的问题。整个压缩包共含1080个文件体积约4.79MB其中JavaScript脚本数量最多622个TypeScript与TSX文件共42个JSON配置文件108个Markdown文档151个并辅以样式、许可证及工程配置文件。这些文件中ts/tsx实现组件与业务逻辑js脚本负责构建工具与运行时命令json管理依赖和项目参数md提供使用说明与开发文档其他配置则用于ESLint校验、Babel转译和编辑器规范整体文件结构清晰便于定位与修改。目前已有335人学习下载。借助该资源读者可深入理解Create React App的工程结构掌握npm start、npm test、npm run build等命令的用法并能基于模板进行二次开发是学习React与TypeScript工程化实践的理想起点。1. 为什么把阿迪达斯当数据项目练手一个品牌数据闭环的样板手里攒了三个月的阿迪达斯线上价格快照想判断双 11 前该不该补货大概是很多电商运营和数据分析师会遇到的真实场景。阿迪达斯的商品线很宽SKU 命名规则不统一折扣策略复杂评论里又大量掺杂尺码、做工、物流的抱怨恰好覆盖了数据采集、清洗、建模、决策的完整链条。我想说的是把“阿迪达斯”当作一个数据项目的标题不等于研究品牌营销而是把它当作一套可复现的技术样板怎样把公开商品数据变成销量预测再把评论情感分析结果反哺选品和补货。这篇文章适合电商数据分析、供应链开发以及刚入行的数据从业者照着落地。2. 数据采集先解决“阿迪达斯有哪些货”的问题在动手建模之前得先把数据管道跑通。常见做法是拿公开接口或授权导出文件拉取商品基础信息、价格和评论落地成表。我不会一上来就用复杂的爬虫框架而是用 requests 加 pandas 快速验证数据形态再决定要不要上定时任务。选数据源的判断标准就两条字段够用更新频率至少每天一次。字段够用指能覆盖商品 ID、标题、原价、促销价、库存状态、评论数、评分、评论内容。更新频率决定后续预测模型能不能跟上真实世界一个每天只更新一次的数据源比你每周手动导一次 Excel 要可靠得多。2.1 数据源评估与合规边界先说合规我建议只使用有授权的开放接口、官方数据下载或你所在公司内部的合法数据不去逆向加密接口不绕过风控也不去碰需要登录才能看到的数据。原因很简单一旦接口地址和签名算法变动采集脚本就废了而且法律风险完全不值得。数据源满足上面两个条件就可以开工不追求高大全。下面这个字段表是我在模拟项目里常用的最小集可以按实际数据源增删。这里的“评论数”先提一句后面特征工程会把它当作销量代理但代理有滞后避坑章节会展开。字段类型用途product_idstr/int唯一主键关联评论与销量titlestr提取系列、颜色、货号original_pricefloat计算折扣率promo_pricefloat计算促销强度可能为空stock_statusstr是否缺货作为短期需求信号review_countint作为销量代理变量ratingfloat商品口碑基线review_textstr后续情感分析原材料字段确认后我会先把数据存成 SQLite 或者 Parquet方便后续特征工程。不要一开始就接分布式存储单表几十万的量级跑 pandas 完全够用。存储路径和字段命名统一放到一个配置文件里否则后期换字段会很痛苦。2.2 用 Python 获取商品列表与详情假设你已经拿到了一个返回 JSON 的公开接口用下面的代码拉取商品列表。注意把地址换成真实数据源这里只是示例骨架。import requests import pandas as pd import time def fetch_products(category_id, page1, page_size50): # 替换为实际授权数据源的地址 url https://api.example.com/adi/products params { category: category_id, # 产品线训练、运动生活、三叶草等 page: page, # 页码从 1 开始 page_size: page_size, # 每页数量建议 20-50 sort: review_count_desc # 按评论数排序优先拿热销品 } headers {User-Agent: research-script/1.0} resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() # 非 2xx 直接抛错 data resp.json() # 接口一般会返回 items 和 total按实际结构调整 return data[items], data[total] items, total fetch_products(running, page1) df pd.DataFrame(items) print(df.shape)category_id是阿迪达斯内部产品线分类不同系列的价格策略和补货周期差异很大后面做特征时要保留这个字段。page_size不建议超过 50很多接口超过之后会拒绝响应。timeout10防止某个请求卡死整个任务。如果接口做了限流在循环里加上time.sleep(0.5)之类的间隔不能暴力重试。商品列表拿到后通常还需要每个商品的详情和评论。详情接口一般接受单个 product_id因此要对列表做循环请求。我习惯用ThreadPoolExecutor把并发控制在 4-8 个避免被限流封 IP。并发不是这个阶段的重点但要记住一个原则宁可慢一点也不要让批量任务在半夜因为超时重试把自己打死。2.3 请求失败重试与日志记录网络请求不会每次都成功所以必须有重试和日志。这里写一个简单的带重试的请求函数用 tenacity 实现指数退避。import time import logging from tenacity import retry, stop_after_attempt, wait_exponential logging.basicConfig(levellogging.INFO) logger logging.getLogger(adi_crawler) retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10)) def get_with_retry(url, params, headersNone): resp requests.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json()重试函数给了三次机会指数退避等待前两次间隔约 2 秒、4 秒最大 10 秒。这比固定 sleep 5 秒更温和不会在接口临时抖动时把服务打挂。日志里记下商品 ID 和重试次数方便排查哪一类请求更容易失败。如果同一个商品连续重试三次仍失败就把它写入失败清单本轮跳过下一轮再补拉。2.4 获取评论数据与翻页控制评论接口的翻页参数通常是product_id、page、page_size。为了不让数据量爆炸我只取最近 5 页评论每页 20 条也就是最新 100 条。这样既能覆盖近期用户反馈又能控制存储。def fetch_reviews(product_id, max_pages5, page_size20): reviews [] for page in range(1, max_pages 1): url https://api.example.com/adi/reviews params { product_id: product_id, page: page, page_size: page_size, sort: time_desc } resp requests.get(url, paramsparams, timeout10) if resp.status_code ! 200: print(f第 {page} 页失败停止翻页) break data resp.json() items data.get(items, []) reviews.extend(items) if len(items) page_size: break # 没有更多数据 time.sleep(0.5) # 限流间隔 return reviewsmax_pages限制最大翻页数防止某个商品评论几十万条导致任务挂住。sorttime_desc保证拿到的评论是按时间倒序排列这样截取前 5 页就是最新评论。break在两个位置出现一是请求失败时停掉二是不足一页时说明到底了。时间间隔 0.5 秒是通用值实际要根据数据源给出的限流响应头调整如果接口明确要求每秒最多一次就不要强行加速。评论和商品表通过product_id关联。把两张表分别导出成 CSV 或 Parquet。导出前先做一次主键去重同一商品 ID 只保留最新一条否则后面 merge 会产生笛卡尔积。2.5 增量更新与主键去重每天跑定时任务时如果直接重新拉全量数据既慢又容易触发限流。常见做法是按更新时间只拉当天有变动的商品。如果数据源没有提供updated_at字段就以review_count或price变化为依据把上一次的快照和本次拉取结果合并。old_df pd.read_csv(adi_products.csv, dtype{product_id: str}) new_df pd.read_csv(adi_products_today.csv, dtype{product_id: str}) merged pd.concat([old_df, new_df], ignore_indexTrue) # 同一商品保留最新记录简单做按时间取最大 merged[updated_at] merged[updated_at].fillna(1900-01-01) merged merged.sort_values(updated_at).drop_duplicates(product_id, keeplast) merged.to_csv(adi_products.csv, indexFalse)这段代码的关键在于drop_duplicates的keeplast。按更新时间排序后最后一条就是最新快照。假如新拉的数据里没有更新某些商品旧的记录会被保留不会因为合并且丢了数据。注意dtype{product_id: str}防止 ID 被 pandas 读成数字尤其像阿迪达斯货号这种带字母的内容必须按字符串处理。至此数据采集这一层稳定了后面的特征工程和模型才有的放矢。整个环节里我最看重去重这一步因为历史上吃过“重复记录导致训练集和验证集泄漏”的亏后面避坑章节会再提。3. 数据清洗与特征工程把“红色三叶草卫衣”变成结构化字段原始标题像“adidas Originals 三叶草 男款 运动卫衣 红色 货号 ABC123”这种数据没法直接进模型需要拆成系列、颜色、价格带、货号等字段。同时销量通常不是直接给出的要用评论增量构造代理变量。特征工程决定模型上限这句话在阿迪达斯这类 SKU 长尾数据上体现得很明显。3.1 SKU 规范化从标题提取系列、颜色与货号阿迪达斯的货号一般是“字母数字字母”的组合例如 FX1203、HQ4518。用正则可以粗提但会混入其他品牌。我会先做系列映射再做货号抽取。系列映射用白名单避免把同样出现“neo”的其他商品误判。import re series_patterns { Originals: [三叶草, originals], Performance: [performance, adidas 运动], NEO: [neo, neolabel] } def extract_series(title): for series, keywords in series_patterns.items(): for kw in keywords: if kw.lower() in title.lower(): return series return unknown def extract_article_no(title): # 典型货号1-3 字母 4 位数字 1-3 字母如 FX1203 m re.search(r[A-Za-z]{1,3}\d{4}[A-Za-z]{0,3}, title) return m.group(0).upper() if m else Noneextract_series是白名单匹配避免把同样出现“neo”的其他商品误判。货号正则写在注释里匹配范围足够用。实际数据里有些标题不带货号我会把article_no为空的记录单独放一边不直接丢弃因为可能是录入问题。颜色提取需要维护一个颜色词表因为阿迪达斯的颜色描述非常多变“红色/白色/黑色”会以“红/白/黑”出现。更稳妥的做法是找颜色词在标题中的位置但为了简洁通常直接用包含关系判断color_map { red: [红, red, crimson], white: [白, white], black: [黑, black, core black], blue: [蓝, blue, navy] } def extract_color(title): for color, keywords in color_map.items(): for kw in keywords: if kw.lower() in title.lower(): return color return other这段代码有个小坑white会匹配到whitesmoke之类的词但阿迪达斯标题里很少出现这种颜色所以问题不大。如果数据源里有英文长标题颜色词表需要扩展。颜色不要做成多标签一个商品标题如果出现多个颜色取第一个匹配的即可颜色组合的建模价值不大。3.2 价格与促销特征折扣率、价格带宽、价格波动价格是预测销量的强特征但直接用原价和促销价不够。我会构造三个特征折扣率、价格带宽、近 7 日均价偏移。折扣率反映促销力度价格带宽反映商品历史价格弹性短期均价偏移反映近期的价格趋势。df[discount_rate] 1 - df[promo_price] / df[original_price] price_stats df.groupby(product_id)[original_price].agg([max, min, mean]).rename( columns{max: price_max, min: price_min, mean: price_mean} ) df df.merge(price_stats, onproduct_id) df[price_band] (df[price_max] - df[price_min]) / df[price_mean] # 近 7 日均价需要时间窗假设 df 有 date 字段 recent7 df[df[date] df[date].max() - pd.Timedelta(days7)] recent_price recent7.groupby(product_id)[promo_price].mean().rename(price_7d_avg) df df.merge(recent_price, onproduct_id, howleft) df[price_7d_offset] df[price_7d_avg] / df[price_mean] - 1discount_rate会出现超过 1 的情况因为促销价偶尔被错误录成优惠券叠加后的价格这种异常值需要在避坑章节处理。price_band衡量一个商品的价格波动范围阿迪达斯有些联名款价格波动很小有些基础款经常变价这个特征能区分两类商品。price_7d_offset反映短期价格趋势补货决策很看重这个。这里还有一个容易被忽略的点价格特征要按商品 ID 分组计算而不是全表计算。如果全表合并一个商品会带上其他商品的价格统计相当于泄漏了整个市场的价格分布但每个商品自身的价格区间才是模型该学的内容。3.3 构造时间特征与预测标签如果没有直接销量字段我会用评论增量作为代理。评论数每天都在涨涨得越快通常销量越高但延迟 1-3 天所以建模时要考虑滞后。先构造滞后特征再构造目标标签。df df.sort_values([product_id, date]) df[review_count_lag1] df.groupby(product_id)[review_count].shift(1) # 目标未来 7 天评论增量注意用了未来数据只能作为标签 df[target_7d] df.groupby(product_id)[review_count].transform( lambda x: x.shift(-7) - x ) # 时间特征 df[weekday] df[date].dt.weekday df[month] df[date].dt.month df[days_to_promotion] (promotion_date - df[date]).dt.daysshift(1)构造前一天评论数避免把当天的评论数直接当作特征否则就是拿未来预测当下。target_7d是未来 7 天的评论增量shift(-7)用了未来数据只能用于打标签绝不能放进特征。days_to_promotion需要你知道下一次大促的日子比如双 11、618对阿迪达斯这种折扣率高的品牌这个特征说服力很强。距离大促越近用户可能等待下单预测模型得捕捉这个心理。特征做完我建议打印一个数据质量报告每个特征的空值数量、唯一值数量、最值。空值多的特征要么删要么填充。比如price_7d_avg在刚上架的新品里会有空值填充成 0 还是用前向填充会直接影响模型对新品的行为需要根据业务逻辑决定。4. 销量与库存预测用 LightGBM 跑通最小可行模型预测目标很直接未来 7 天每个商品的评论增量也就是代表销量的代理值。直接用线性回归也能跑但面对促销日、断货、季节变动这些非线性情况LightGBM 的鲁棒性更好而且不需要做太复杂的数据标准化。这里的核心不是堆模型而是把时间切分、特征泄漏、业务约束都处理好。4.1 问题定义回归还是分类回归预测具体的增量数字分类则预测“需不需要补货”。我建议先从回归开始因为回归的误差能反映预测质量后面再根据业务阈值转分类。比如预测增量大于 20 就视为需要补货等于给回归结果加了一个决策边界。建模之前特征列要统一类型。常见特征包括discount_rate、price_band、price_7d_offset、review_count_lag1、weekday、month、days_to_promotion、series类别编码。这里有一个容易犯的错把target_7d也当作特征放进去这会导致数据泄漏模型在训练集上完美上线后完全失效。4.2 最小训练代码我用 LightGBM 的 sklearn 接口代码很短。关键点在于时间序列切分和提前停止而不是模型本身。import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error import numpy as np features [discount_rate, price_band, price_7d_offset, review_count_lag1, weekday, month, days_to_promotion, series] X df[features].copy() y df[target_7d].copy() # 时间序列切分按日期排序后分 3 折每折的训练集都在验证集之前 tscv TimeSeriesSplit(n_splits3) errors [] for train_idx, val_idx in tscv.split(X): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model lgb.LGBMRegressor( n_estimators300, learning_rate0.05, num_leaves31, min_child_samples20, random_state42 ) model.fit( X_train, y_train, eval_set[(X_val, y_val)], eval_metricmae, callbacks[lgb.early_stopping(50), lgb.log_evaluation(50)] ) pred model.predict(X_val) errors.append(mean_absolute_error(y_val, pred)) print(fMAE: {np.mean(errors):.3f})TimeSeriesSplit是关键它按顺序切分训练集永远在验证集之前避免随机切分带来的未来信息。early_stopping(50)防止过拟合当验证集 MAE 连续 50 轮不下降就停止。min_child_samples控制叶子节点最少样本量对阿迪达斯这种长尾数据很有用因为很多商品销量稀疏叶子节点样本太少会学噪声。注意如果series是字符串LightGBM 的 sklearn 接口不会自动把它当作分类特征需要提前用LabelEncoder或pd.Categorical转换否则会直接报错。4.3 参数调节与验证指标LightGBM 的调参顺序一般先固定学习率调num_leaves再调min_child_samples最后调特征抽样。给一个常用范围参数取值范围作用num_leaves15-127控制模型复杂度越大越容易过拟合learning_rate0.01-0.1越小越稳但需要更多树min_child_samples10-50越大越保守适合稀疏销量feature_fraction0.7-0.9降维减少特征间相关性影响n_estimators结合 early stopping不需要手调太多验证指标我习惯同时看 MAE 和 MAPE。MAE 告诉你平均差多少个评论MAPE 告诉你相对误差百分比。阿迪达斯有些商品评论基数很小MAPE 会失真所以要按评论数分组看误差——把商品按过去 30 天评论增量分档分别计算 MAE这样能发现模型在低销量商品上是否离谱。如果 MAE 很大先查特征里有没有泄漏再查库存状态。缺货商品评论增量会突然跌到接近 0模型如果没学到库存特征预测会偏高很多。所以建议把stock_status作为特征加进去缺货时模型输出应该自动压低。4.4 特征重要性与模型保存训练完别急着收工看一眼特征重要性。LightGBM 可以直接用model.feature_importances_拿到。如果review_count_lag1的重要性远高于其他特征说明模型在靠昨天的销量猜明天这不算错但要意识到一旦评论采集中断这个特征失效后整个预测都会翻车。模型保存用joblib.dump加载时保持特征列顺序一致。保存前再把训练日期、特征列表、参数记录到 JSON 里否则三个月后你看模型文件根本不知道它当时用了哪些列。我吃过这个亏重新部署时因为特征顺序不一致导致预测结果完全乱掉后来养成了写 model_meta.json 的习惯。5. 避坑指南阿迪达斯数据项目的五个常见坑这章把我在模拟项目和类似数据处理里踩过的坑集中说一下按现象、原因、解决的顺序写。条目之间按影响程度排列越靠前的越隐蔽。5.1 商品数据更新滞后导致预测失效现象模型上线后连续三天预测销量偏高仓库备货不足补货跟不上真实需求。 原因采集任务每天早上 8 点拉取数据但某些接口的数据实际上延迟一天以上价格和库存状态是滞后的模型拿到的输入和真实世界错位。 解决给每条记录增加collected_at字段用数据源提供的updated_at判断新鲜度滞后超过 24 小时的数据不进训练集。定时任务启动前先拉取一个健康检查接口确认数据时间戳是最新的。另外把预测结果和真实值画在同一张趋势图上如果早上 8 点预测晚上 8 点回看偏差连续三天偏差方向一致就要怀疑输入数据滞后而不是模型坏了。5.2 货号正则把非阿迪达斯商品混进来现象特征工程后 SKU 总数比商品总数多出 5%部分系列出现奇怪的货号。 原因货号正则[A-Za-z]{1,3}\d{4}[A-Za-z]{0,3}会匹配到标题中类似货号的片段例如数字尺码“43 1/3”或者颜色编码被误判。 解决不用纯正则先做品牌词过滤再取正则结果时要求货号出现在标题后半段同时检查货号在官方数据库中的前缀。维护一个已知货号前缀白名单例如阿迪达斯跑鞋常见的FX、GW、HQ。如果白名单太窄会漏掉新款所以白名单只用于异常校验不用于强过滤。5.3 折扣率超过 100% 的异常价格现象双 11 前数据里出现折扣率 1.35 的商品训练时模型权重被拉偏。 原因促销价字段被录成“券后价”或“叠加满减价”而原价是吊牌价导致价格倒挂。 解决对promo_price original_price * 0.8且discount_rate 0.7的记录标记为异常用前后 7 天同类产品的平均折扣率填充。如果异常比例超过 1%优先检查数据源字段定义必要时废弃该字段不用。我在模拟项目里就是直接把异常行删除结果把大促前的真促销样本也删了模型反而学不到促销规律。5.4 用评论增量当销量代理的滞后问题现象新上架的商品评论数为 0评论增量预测为负模型低估真实销量。 原因评论采集有延迟且用户购买后到评论需要几天时间新品的评论增量从 0 到 1 需要一段冷启动。 解决对上市不足 14 天的商品单独建模使用搜索热度、收藏数等替代特征。如果没有这些特征就把days_on_shelf作为特征并避免对新品用同一阈值判断补货。冷启动期的商品宁可人工复核也不要依赖一个销量代理的回归结果。5.5 随机切分导致验证集被未来信息污染现象用train_test_split做验证MAE 很低但上线后误差扩大一倍。 原因随机切分会让同一天的商品既出现在训练集又出现在验证集模型记住了时间相关的模式换到未来时就失效。 解决全部改用TimeSeriesSplit或按日期切片训练集日期必须早于验证集日期。另外特征工程里的shift操作也要按product_id分组进行避免跨商品错位。最后还要检查训练集和验证集是否有同一个商品 ID 出现因为重复商品跨时间也会泄漏价格和评论数的滞后信息。这些坑不一定同时出现但只要你把阿迪达斯数据项目跑上两个月至少会碰见其中两三个。最让我印象深刻的是第 5 个当时我以为随机切分没问题结果花了整整一周排查上线误差最后发现根因是验证集里混了未来时间戳的数据。6. 进阶把评论情感分析结果接入补货决策模型和避坑都跑通后我想分享一个更上层的玩法把对阿迪达斯评论的情感分析结果和销量预测放在同一张决策表里。6.1 从评论中定位“尺码偏小”之类的高频痛点用简单的关键词和情感极性就可以完成第一版。把评论按商品维度做分词统计“尺码偏小”“磨脚”“闷热”这类负面短语的出现频率。情感极性可以用现成的规则词典不用一上来就微调大模型。负面率就是负面评论数除以总评论数样本少于 10 条的按默认值处理不参与排序。得到每个商品的情感负面率作为特征或辅助指标。6.2 用预测结果和情感分数做补货优先级把模型预测结果导出成表再按商品 ID 关联情感负面率最后按下面这组规则生成动作预测增量负面率高负面率低高谨慎补货优先补货低不补货维持库存这个规则本质是给预测结果加一个风险约束避免把资金押在口碑差的爆款上。我自己在做模拟项目时曾忽略情感分数只顾销量预测结果补了一批评论里全是“尺码偏小”的畅销货退货率居高不下。后来把情感分析结果接入补货规则情况明显好转。希望帮到你。本文还有配套的精品资源点击获取