简介:这是一个由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_id | str/int | 唯一主键,关联评论与销量 |
| title | str | 提取系列、颜色、货号 |
| original_price | float | 计算折扣率 |
| promo_price | float | 计算促销强度,可能为空 |
| stock_status | str | 是否缺货,作为短期需求信号 |
| review_count | int | 作为销量代理变量 |
| rating | float | 商品口碑基线 |
| review_text | str | 后续情感分析原材料 |
字段确认后,我会先把数据存成 SQLite 或者 Parquet,方便后续特征工程。不要一开始就接分布式存储,单表几十万的量级跑 pandas 完全够用。存储路径和字段命名统一放到一个配置文件里,否则后期换字段会很痛苦。
2.2 用 Python 获取商品列表与详情
假设你已经拿到了一个返回 JSON 的公开接口,用下面的代码拉取商品列表。注意把地址换成真实数据源,这里只是示例骨架。
import requests import pandas as pd import time def fetch_products(category_id, page=1, page_size=50): # 替换为实际授权数据源的地址 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, params=params, headers=headers, timeout=10) resp.raise_for_status() # 非 2xx 直接抛错 data = resp.json() # 接口一般会返回 items 和 total,按实际结构调整 return data["items"], data["total"] items, total = fetch_products("running", page=1) df = pd.DataFrame(items) print(df.shape)category_id是阿迪达斯内部产品线分类,不同系列的价格策略和补货周期差异很大,后面做特征时要保留这个字段。page_size不建议超过 50,很多接口超过之后会拒绝响应。timeout=10防止某个请求卡死整个任务。如果接口做了限流,在循环里加上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(level=logging.INFO) logger = logging.getLogger("adi_crawler") @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10)) def get_with_retry(url, params, headers=None): resp = requests.get(url, params=params, headers=headers, timeout=10) 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_pages=5, page_size=20): 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, params=params, timeout=10) 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限制最大翻页数,防止某个商品评论几十万条导致任务挂住。sort="time_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_index=True) # 同一商品保留最新记录:简单做按时间取最大 merged["updated_at"] = merged["updated_at"].fillna("1900-01-01") merged = merged.sort_values("updated_at").drop_duplicates("product_id", keep="last") merged.to_csv("adi_products.csv", index=False)这段代码的关键在于drop_duplicates的keep="last"。按更新时间排序后,最后一条就是最新快照。假如新拉的数据里没有更新某些商品,旧的记录会被保留,不会因为合并且丢了数据。注意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, on="product_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(days=7)] recent_price = recent7.groupby("product_id")["promo_price"].mean().rename("price_7d_avg") df = df.merge(recent_price, on="product_id", how="left") 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_splits=3) 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_estimators=300, learning_rate=0.05, num_leaves=31, min_child_samples=20, random_state=42 ) model.fit( X_train, y_train, eval_set=[(X_val, y_val)], eval_metric="mae", callbacks=[lgb.early_stopping(50), lgb.log_evaluation(50)] ) pred = model.predict(X_val) errors.append(mean_absolute_error(y_val, pred)) print(f"MAE: {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_leaves | 15-127 | 控制模型复杂度,越大越容易过拟合 |
| learning_rate | 0.01-0.1 | 越小越稳但需要更多树 |
| min_child_samples | 10-50 | 越大越保守,适合稀疏销量 |
| feature_fraction | 0.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 关联情感负面率,最后按下面这组规则生成动作:
| 预测增量 | 负面率高 | 负面率低 |
|---|---|---|
| 高 | 谨慎补货 | 优先补货 |
| 低 | 不补货 | 维持库存 |
这个规则本质是给预测结果加一个风险约束,避免把资金押在口碑差的爆款上。我自己在做模拟项目时,曾忽略情感分数只顾销量预测,结果补了一批评论里全是“尺码偏小”的畅销货,退货率居高不下。后来把情感分析结果接入补货规则,情况明显好转。希望帮到你。
本文还有配套的精品资源,点击获取