先贴上核心代码。
def fit_transform_sparse(
df_fit: pd.DataFrame,
df_tr: pd.DataFrame,
df_va: pd.DataFrame,
text_cols=("name", "item_description"),
cat_cols=("brand_name", "cat1", "cat2", "cat3"),
num_cols=("item_condition_id", "shipping"),
) -> Tuple[csr_matrix, csr_matrix, Dict]:
"""
在 df_fit 上拟合 TF-IDF 和 OHE(可用 df_fit=df_tr 或全量),
并对 df_tr / df_va 进行 transform。
返回:X_tr_sparse, X_va_sparse, fitted_objs
"""
log("TF-IDF: 拟合 name")
name_vec = TfidfVectorizer(
max_features=NAME_MAX_FEATS, ngram_range=(1, 2),
min_df=TFIDF_MIN_DF, dtype=np.float32
)
# 用 df_fit 拟合词表,再分别 transform 训练/验证
_ = name_vec.fit_transform(df_fit[text_cols[0]].astype(str)) # 对训练集df_fit进行训练+转换,因为不需要转换后的结果,所以用_代替
Xtr_name_tr = name_vec.transform(df_tr[text_cols[0]].astype(str)) # 利用前一步训练好的模型转换训练集,这个是真正希望的结果
Xtr_name_va = name_vec.transform(df_va[text_cols[0]].astype(str)) # 利用前一步训练好的模型转换验证集
log(f"TF-IDF[name] -> tr={Xtr_name_tr.shape}, va={Xtr_name_va.shape}")
log("TF-IDF: 拟合 description")
desc_vec = TfidfVectorizer(
max_features=DESC_MAX_FEATS, ngram_range=(1, 2),
min_df=TFIDF_MIN_DF, dtype=np.float32
)
_ = desc_vec.fit_transform(df_fit[text_cols[1]].astype(str))
Xtr_desc_tr = desc_vec.transform(df_tr[text_cols[1]].astype(str))
Xtr_desc_va = desc_vec.transform(df_va[text_cols[1]].astype(str))
log(f"TF-IDF[desc] -> tr={Xtr_desc_tr.shape}, va={Xtr_desc_va.shape}")
log("OHE: 拟合分类特征")
ohe = OneHotEncoder(handle_unknown="ignore", sparse_output=True, dtype=np.float32)
ohe.fit(df_fit[list(cat_cols)].astype(str))
Xtr_c_tr = ohe.transform(df_tr[list(cat_cols)].astype(str))
Xtr_c_va = ohe.transform(df_va[list(cat_cols)].astype(str))
log(f"OHE -> tr={Xtr_c_tr.shape}, va={Xtr_c_va.shape}")
# 数值列
def to_sparse_numeric(df, cols):
arr = df[list(cols)].astype(np.float32).values
return csr_matrix(arr)
Xtr_n_tr = to_sparse_numeric(df_tr, num_cols)
Xtr_n_va = to_sparse_numeric(df_va, num_cols)
# 拼接(稀疏)
log("拼接稀疏特征(name + desc + cat + num)")
X_tr = hstack([Xtr_name_tr, Xtr_desc_tr, Xtr_c_tr, Xtr_n_tr], format="csr")
X_va = hstack([Xtr_name_va, Xtr_desc_va, Xtr_c_va, Xtr_n_va], format="csr")
log(f"X_tr shape={X_tr.shape}, mem={csr_mem_mb(X_tr):.1f} MB | X_va shape={X_va.shape}, mem={csr_mem_mb(X_va):.1f} MB")
# 释放中间块(局部变量会在函数返回后被释放)
return X_tr, X_va, {
"name_vec": name_vec,
"desc_vec": desc_vec,
"ohe": ohe,
"text_cols": text_cols,
"cat_cols": cat_cols,
"num_cols": num_cols,
}
核心的特征工程的代码被封装成了一个函数,我们一块块进行分析。
1. TF-IDF拟合商品名称Name和商品描述Description
这个项目的特点是数据字段不多,总共就如下几个字段:
- name:商品名称,字符串
- item_description:商品描述,字符串
- category_name:类别名称,分类
- brand_name:品牌名称,字符串
- shipping:运费承担方,数值
- item_condition_id:新旧程度,数值
- price:价格,数值
因此,作为字符串类型的商品名称、商品描述字段蕴含了十分有用的信息,我们需要从中提取有用信息,提取的方式就是用TF-IDF。
要讲TF-IDF,就得先补充一下CountVectorizer,先说结论一句:
- CountVectorizer:把文本变成“每个词出现了几次”的计数向量。
- TF-IDF(+ TfidfVectorizer):在“计数”的基础上,再考虑“这个词在整个语料中有多常见”,给常见词降权、给有区分力的词加权。
1.1 CountVectorizer 是什么
思想:
先把所有文本里出现过的词收集成一个“词表”(vocabulary),然后每一条文本就变成一个向量:
第 i 维 = 这个文本中第 i 个词出现的次数。
特点:
-
非常直观,就是词频统计(Term Frequency,TF)。
-
得到的是 稀疏矩阵(大多数位置都是 0)。
-
没有考虑词的重要程度,“的、了、and、the” 这类高频词会有很大权重。
1.2 TF-IDF 是什么?
TF-IDF = TF × IDF
-
TF(Term Frequency):词在当前文档中的频率
- 最简单:
TF = 该词在文档中出现次数 / 文档总词数
- 最简单:
-
IDF(Inverse Document Frequency,逆文档频率):词在整个语料库中“多常见”的一个反比指标
-
常见公式:
IDF(t)=logN+1df(t)+1+1\text{IDF}(t) = \log\frac{N + 1}{df(t) + 1} + 1IDF(t)=logdf(t)+1N+1+1 -
NNN:文档总数
-
df(t)df(t)df(t):包含词 t 的文档数
-
直觉:
-
某个词在一篇文档内很频繁 → TF 高
-
但如果这个词在所有文档里都很常见 → IDF 低(没啥区分力)
-
真正重要的词:在这篇文档中高频,但在其他文档中不太常见 → TF 高、IDF 也高,TF-IDF 就大。
所以 TF-IDF 实际上是在做:
“降权”停用词/常见词,
“抬高”那些能区分不同文档的关键词。
1.3 TfidfVectorizer 是什么?
在 sklearn 里,TfidfVectorizer = CountVectorizer + TF-IDF 权重:
-
分词、构建词表
-
计算每条文本每个词的计数(类似 CountVectorizer)
-
再根据全体语料计算 IDF
-
输出的矩阵元素 = TF × IDF(通常还会做 L2 归一化)
1.4 CountVectorizer vs TF-IDF:什么时候用哪个?
CountVectorizer 更适合:
-
模型本身可以处理“词频大小”问题,比如:
-
朴素贝叶斯(有时喜欢用原始计数)
-
一些基于词共现的简单统计
-
-
你后续会自己做其他权重/归一化处理。
TF-IDF 更适合:
-
文本分类、检索、相似度计算等大多数传统 NLP 任务(SVM、LR、KNN、线性模型等)。
-
希望减弱高频无意义词的影响,让“区分度高的词”更突出。
-
Kaggle 上很多经典文本题(例如 Mercari)一开始就用 TF-IDF 作为 baseline 特征。
可以简单记:
“做文本分类/检索/相似度 → 先上 TF-IDF;
想纯统计、或后面自己做权重 → 可以用 CountVectorizer。”
好了,那回到代码本身:
log("TF-IDF: 拟合 name")
name_vec = TfidfVectorizer(
max_features=NAME_MAX_FEATS, ngram_range=(1, 2),
min_df=TFIDF_MIN_DF, dtype=np.float32
)
# 用 df_fit 拟合词表,再分别 transform 训练/验证
_ = name_vec.fit_transform(df_fit[text_cols[0]].astype(str)) # 对训练集df_fit进行训练+转换,因为不需要转换后的结果,所以用_代替
Xtr_name_tr = name_vec.transform(df_tr[text_cols[0]].astype(str)) # 利用前一步训练好的模型转换训练集,这个是真正希望的结果
Xtr_name_va = name_vec.transform(df_va[text_cols[0]].astype(str)) # 利用前一步训练好的模型转换验证集
log(f"TF-IDF[name] -> tr={Xtr_name_tr.shape}, va={Xtr_name_va.shape}")
这块代码对字段name完成了如下功能:
- 实例化了一个
TfidfVectorizer类name_vec - 利用训练集
df_fit进行预训练name_vec - 利用训练好的
name_vec对真正的训练集df_tr和验证集df_va进行转换
然后,对item_description做同样的处理。
然后,我们发现,所有的字段中,处理状态如下:
name:商品名称,字符串item_description:商品描述,字符串- category_name:类别名称,分类
- brand_name:品牌名称,字符串
- shipping:运费承担方,数值
- item_condition_id:新旧程度,数值
- price:价格,数值
接下来,我们来处理类别字段。
2. OHE处理类别名称
由于类别名称字段我们在前一篇文章已经进行了处理,所以,我们对其进行独热编码(OHE)即可。
log("OHE: 拟合分类特征")
ohe = OneHotEncoder(handle_unknown="ignore", sparse_output=True, dtype=np.float32)
ohe.fit(df_fit[list(cat_cols)].astype(str))
Xtr_c_tr = ohe.transform(df_tr[list(cat_cols)].astype(str))
Xtr_c_va = ohe.transform(df_va[list(cat_cols)].astype(str))
log(f"OHE -> tr={Xtr_c_tr.shape}, va={Xtr_c_va.shape}")
这块代码对字段category_name完成了如下功能:
- 实例化了一个
OneHotEncoder类ohe - 利用训练集
df_fit进行预训练ohe - 利用训练好的
ohe对真正的训练集df_tr和验证集df_va进行转换
然后,我们发现,所有的字段中,处理状态如下:
name:商品名称,字符串item_description:商品描述,字符串category_name:类别名称,分类brand_name:品牌名称,字符串- shipping:运费承担方,数值
- item_condition_id:新旧程度,数值
- price:价格,数值
接下来,我们来处理数值类型字段。
3. 数值列进行稀疏处理
在对name和item_description进行TF-IDF处理以及对category_name进行了OHE处理后,两者都形成了稀疏矩阵,也即矩阵中的值大部分是0,几少量有真正的值。因此,为了后续用hstack进行拼接,我们对数值类型的字段也进行稀疏处理。
# 数值列
def to_sparse_numeric(df, cols):
arr = df[list(cols)].astype(np.float32).values
return csr_matrix(arr)
Xtr_n_tr = to_sparse_numeric(df_tr, num_cols)
Xtr_n_va = to_sparse_numeric(df_va, num_cols)
后面我会专门整理关于稀疏化的专题,这里先不展开,大家只需要理解前面几个数据集都稀疏化了,为了统一,数值类型的字段也进行稀疏化。
然后,我们发现,所有的字段中,处理状态如下:
name:商品名称,字符串item_description:商品描述,字符串category_name:类别名称,分类brand_name:品牌名称,字符串shipping:运费承担方,数值item_condition_id:新旧程度,数值- price:价格,数值
价格字段属于我们要预测的值,所以就不再处理了,至此,我们初步的特征工程就结束了,接下来把所有的系数矩阵拼接起来就完成了。
# 拼接(稀疏)
log("拼接稀疏特征(name + desc + cat + num)")
X_tr = hstack([Xtr_name_tr, Xtr_desc_tr, Xtr_c_tr, Xtr_n_tr], format="csr")
X_va = hstack([Xtr_name_va, Xtr_desc_va, Xtr_c_va, Xtr_n_va], format="csr")
log(f"X_tr shape={X_tr.shape}, mem={csr_mem_mb(X_tr):.1f} MB | X_va shape={X_va.shape}, mem={csr_mem_mb(X_va):.1f} MB")

被折叠的 条评论
为什么被折叠?



