机器学习实践项目(三)- Mercari二手商店价格预测 - 特征工程

先贴上核心代码。

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)=log⁡N+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 权重

  1. 分词、构建词表

  2. 计算每条文本每个词的计数(类似 CountVectorizer)

  3. 再根据全体语料计算 IDF

  4. 输出的矩阵元素 = 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完成了如下功能:

  1. 实例化了一个TfidfVectorizername_vec
  2. 利用训练集df_fit进行预训练name_vec
  3. 利用训练好的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完成了如下功能:

  1. 实例化了一个OneHotEncoderohe
  2. 利用训练集df_fit进行预训练ohe
  3. 利用训练好的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")
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值