销售预测爆旺(scikit-learn版本)

悼念一下回归模型的悲剧,先尝试一下分类模型,稍后再整他

1 数据探索

sparkSQL支持用sql对数据集进行分析,数据探索工作仍然大部分放在spark中来完成

1.1 🔑发现的一些相关性

对应的数值越接近1表示正相关性越大,越接近-1表示负相关性越大,越接近0表示相关性越小

  • 销售额的相关度往往好于销量
    • 毛利率、销量以及库存周转率的权衡在销售额上综合体现了?
    • 销售任务的导向作用?
  • 排除极low款与爆款的前提下
    • 新货前30天预测后30天相关性较大

    • 新货前30天预测整个商品季相关性较大

      • 放开爆款,反而销量的相关度上去了
      • 销售额的相关度有所下降
    • image
    • image
    • image
  • 冬装数据太奇葩了,基本依托于两个大活动走货
    • 考虑要把冬装单独拆分出模型来搞
    • 其它季节货品使用一个预测模型
    • 只保留冬季的情况


      image

1.2 决定尝试分offset构建模型

1.2.1 预测商品级销量分类段划分:offset_total_quantity

Offset(销量) 正分类(大于offset) 负分类(小于offset)
1000 1477 4471
1600 1016 4932
10000 204 5744
50000 16 5932

1.2.2 参考周期划分:

重点调优放在前三个档,因为参考周期太长,预测的意义也就小了

  • 前3天:offset3_quantity
  • 前7天:offset7_quantity
  • 前15天:offset15_quantity
  • 前30天:offset30_quantity

2 开撸

代码的注释基本都用的英文,不是为了装逼,是怕有字符集兼容问题。。。

2.1 包引入

大致分为三类: 数据操作类、sklearn相关、可视化相关。

# package import
from math import log
import pandas as pd
from pandas import DataFrame
import numpy as np 
from string import Template

from sklearn import preprocessing
from sklearn.preprocessing import OneHotEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score,f1_score
from sklearn.externals import joblib

from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
import plotly.plotly as py
import plotly.graph_objs as go

核心包简介

  • pandas: 数据集读取操作查询转换输出库。
  • sklearn: scikit-learn提供的ML相关方法实现库。
    • preprocessing: 特征预处理相关。
    • model_selection: model所需的数据集选取生成。
    • metrics: 模型效果评估相关方法。
    • externals: 模型持久化相关。
  • plotly: 发现的一个第三方可视化库,比matplotlib操作起来简单,生成图形可以交互分享,但是间歇性被墙。。😂

2. 辅助函数声明

2.1 生成对应offset的类标

类标生成辅助方法,方法会塞入到pandas dataframe的apply方法中,默认会传入row

def gen_hot_product_label(row, offset, column_index):
    """Classification label generator.
    
    Args:
        row: pandas dataframe row.
        offset: Classification offset, such as 1000, 1600, 10000, 50000.
        column_index: Dataframe row[column_index], such as row[12].
    
    Return:
        Result of row_column value above offset. For example:
        
        1: row_column >= offset.
        0: row_column < offset.
    
    """
    if (row[column_index] >= offset):
        return 1
    else:
        return 0

2.2 特征变换

减少特征之间或者特征与类标之间取值差距,blablabla

方法

log辅助方法

def log_quantity(row, column_index):
    """Log the value."""
    return log(row[column_index])

标准化转换

from sklearn.preprocessing import StandardScaler

def standardScalerTransform(X_train, X_test):
    """StandardScaler transform."""
    sc = StandardScaler()
    sc.fit(X_train)
    return (sc.transform(X_train), sc.transform(X_test))

min-Max转换

from sklearn.preprocessing import MinMaxScaler

def minMaxTransform(X_train, X_test):
    """MinMaxScaler transform."""
    sc = MinMaxScaler()
    sc.fit(X_train)
    return (sc.transform(X_train), sc.transform(X_test))

2.3 样本均匀化

前情回顾

正负样本分布不均匀,需要均匀化处理,使得正负样本数基本一致。
隆重介绍imblearn库,提供各种样本均匀化算法的实现。

Offset(销量) 正分类(大于offset) 负分类(小于offset)
1000 1477 4471
1600 1016 4932
10000 204 5744
50000 16 5932

under-sampling

把多的砍掉,正样本多就砍正样本,负样本多就砍负样本的,最后就一致了。
至于如何砍就有很多算法了,这里选用了NearMiss算法。

from imblearn.under_sampling import NearMiss

def under_samplingTransform(X, y):
    """Under-sampling NearMiss mode."""
    return NearMiss(random_state=0, version=1).fit_sample(X, y)

over-sampling

哪种样本少了,就想办法造一些,最后就一致了。
至于如何造就有很多算法了,这里选用了SMOTE的SVM模式算法。

from imblearn.over_sampling import SMOTE, ADASYN

def over_samplingTransform(X, y):
    """Over-sampling SMOTE svm mode."""
    return SMOTE(kind='svm').fit_sample(X, y)

2.4 模型算法

最简单的是感知器算法,因为不能解决线性不可分问题,就忽略掉了。。

逻辑斯蒂回归

唬人的名字,说是回归,其实是分类算法。。
分类界用的很多。

from sklearn.linear_model import LogisticRegression

def logisticRegModelGenerator(train_std, y_train):
    """LogisticRegression model generator."""
    return LogisticRegression(C=1000, random_state=0).fit(train_std, y_train)

随机森林

理论上说可以忽略样本分布不均匀的问题(因为属于决策树类的算法)。

from sklearn.ensemble import RandomForestClassifier  

def random_forest_classifier(train_x, train_y):    
    """Random Forest model generator.""" 
    return RandomForestClassifier(n_estimators=8).fit(train_x, train_y)     

SVM

忽然概念名词超多的算法,什么超平面啥的。。

from sklearn.svm import SVC

def svm_classifier(train_x, train_y):    
    """SVM model generator."""
    return SVC(kernel='rbf', probability=True).fit(train_x, train_y)    

GBDT

梯度提升算法(实测在这个场景综合效果较好😘)

from sklearn.ensemble import GradientBoostingClassifier    
def gradient_boosting_classifier(train_x, train_y):    
    """GBDT model generator."""
    return GradientBoostingClassifier(n_estimators=200).fit(train_x, train_y)    

xgboost

在kaggle大赛中叱咤风云的神级算法,在这个场景实测效果不如GBDT
但xg有些好处,比如可以输出每轮学习时的精确度,以及输出目前输入特征的重要性分数,便于优化调参。

from xgboost import XGBClassifier
from xgboost import plot_importance
from matplotlib import pyplot

def xgboost_classifier(train_x, train_y):
    """xgboost model generator."""
    model = XGBClassifier()
    model.fit(train_x, train_y)
    
    # Feature importance.
    # plot_importance(model)
    # pyplot.show()
    
    return model

2.5 预测类

SalesProphet(销售预言家):预测辅助类
因为各种特征offset、类标、算法的组合,不封装一个类的话,将来会死的。。(已经死过一轮了,改一个东东要累死。。)
具体方法作用详见注释哈,总之就是传入参数,调用predit完事。
(吐槽python 断言竟然只能在继承于testcase的类中使用。。)

class SalesProphet(object):
    """Sales prediction class.
    
    Args:
        datasource: Sales prediction datasource relied on.
        features_name: Feature names array.
        label_name: Label names String.
        model_type: Algorithm of model training.
        X: Feature data.
        y: Label data.
        X_train: X train data.
        X_test: X test data.
        y_train: y train data.
        y_test: y test data.
        y_pred: y data predicted.
        model: ML model fitted.
        accuracy_score: Model accuracy score.
        f1_score: Model f1 score.
        train_score: Model score in train set.
        test_score: Model score in test set.
    """
    
    def __init__(self, datasource, features_name, label_name, model_type='logistic'):
        """Inits SalesProphet with datasource, features_name, label_name, model_type(default is logistic)"""
        self.datasource = datasource
        self.features_name = features_name
        self.label_name = label_name
        self.model_type = model_type
    
    def feature_engineering(self):
        """Feature engineering about: X y generated, one-hot, sampling blabla..."""
        # assertIsNotNone(self.datasource, 'Guys, forget the datasource!!!')
        # assertNotEqual(len(self.features_name), 0, 'features is empty. WTF...')
        # assertNotEqual(len(self.label_name), 0, 'label is empty. WTF...')
        
        self.X = self.datasource[self.features_name].values
        self.y = self.datasource[self.label_name]
        
        # one-hot
        ohe = OneHotEncoder(categorical_features = [0, 1])
        self.X = ohe.fit_transform(self.X).toarray()
        
        self.X, self.y = over_samplingTransform(self.X, self.y)
        
    def train_test_transform(self):
        """Trainset and testset splitor and standard transform."""
        self.X_train, self.X_test, self.y_train, self.y_test = train_test_split(self.X, self.y, test_size=0.3, random_state=0)
        self.X_train, self.X_test = standardScalerTransform(self.X_train, self.X_test)
        
    def model_fitting(self):
        """Algorithm of model selector."""
        model_alg_switcher = {
            'logistic': logisticRegModelGenerator,
            'rf': random_forest_classifier,
            'svm': svm_classifier,
            'gdbt': gradient_boosting_classifier,
            'xgboost': xgboost_classifier
        }
        func = model_alg_switcher.get(self.model_type, logisticRegModelGenerator)
        return func(self.X_train, self.y_train)
    
    def genPredictReport(self, printlog=False):
        """Model estimate report generation. Set printlog YES to pring log."""
        self.accuracy_score = accuracy_score(self.y_test, self.y_pred)
        self.f1_score = f1_score(self.y_test, self.y_pred, average='binary')
        self.train_score = self.model.score(self.X_train, self.y_train)
        self.test_score = self.model.score(self.X_test, self.y_test)
        
        if printlog:
            print('/--------------START-------------')
            print('|')
            print('| feature: %s' % self.features_name)
            print('| label: %s' % self.label_name)
            print('| model alg: %s' % self.model_type)
            print('|')
            print('|----Estimate score------')
            print('|')
            print('| accuracy is: %.2f' % self.accuracy_score)
            print('| f1_score is: %.2f' % self.f1_score)
            print('|')
            print('|---Over-fitting check---')
            print('|')
            print('| train-set score: %.2f' % self.train_score)
            print('| test-set score: %.2f' % self.test_score)
            print('|')
            print('|---------------END--------------/\n')
        
    def public_genReportChart(self):
        """y_test and y_pred chart generation."""
        t = np.arange(len(self.y_pred))
        # Create traces
        trace0 = go.Scatter(
            x = t,
            y = self.y_pred,
            mode = 'lines',
            name = 'predict'
        )

        trace1 = go.Scatter(
            x = t,
            y = self.y_test,
            mode = 'lines',
            name = 'real'
        )

        data = [trace0, trace1]

        py.iplot(data, filename='(%self.features_name)_(%self.labels_name)_(%self.model_type.model)')
    
    def public_saveModel(self):
        """Model persistence."""
        joblib.dump(self.model, '%(self.features_name)_%(self.labels_name)_(%self.model_type.model)')
    
    
    def predict(self):
        """Predict main method."""
        self.feature_engineering()
        self.train_test_transform()
        
        self.model = self.model_fitting()
        self.y_pred = self.model.predict(self.X_test)
        self.genPredictReport()        

3. 数据准备

3.1 数据读取

从spark 导出准备好的数据到csv文件,pandas读取该csv中的数据。

train_data = pd.read_csv("data/product_2016_offset_group.csv")

获取前5条数据看看情况

train_data.head()

describe 可以对df中各列的综合指标进行集中展示。
比如中位数、均值等等,方便进一步分析数据。

train_data.describe()

3.2 销量特征log变换

train_data['log_3_quantity'] = train_data.apply(log_quantity, column_index=8, axis=1)
train_data['log_7_quantity'] = train_data.apply(log_quantity, column_index=9, axis=1)
train_data['log_15_quantity'] = train_data.apply(log_quantity, column_index=10, axis=1)
train_data['log_30_quantity'] = train_data.apply(log_quantity, column_index=11, axis=1)
train_data['log_total_quantity'] = train_data.apply(log_quantity, column_index=12, axis=1)
train_data.head()

3.3 类标生成

train_data['hot_1000_product'] = train_data.apply(gen_hot_product_label, args=(1000, 12), axis=1)
train_data['hot_1600_product'] = train_data.apply(gen_hot_product_label, args=(1600, 12), axis=1)
train_data['hot_10000_product'] = train_data.apply(gen_hot_product_label, args=(10000, 12), axis=1)
train_data['hot_50000_product'] = train_data.apply(gen_hot_product_label, args=(50000, 12), axis=1)
train_data.head()
train_data[train_data.hot_1000_product == 1].count()
product_code                  1477
category_id                   1477
season                        1477
offset3_amount_actual         1477
offset7_amount_actual         1477
offset15_amount_actual        1477
offset30_amount_actual        1477
offset_total_amount_actual    1477
offset3_quantity              1477
offset7_quantity              1477
offset15_quantity             1477
offset30_quantity             1477
offset_total_quantity         1477
log_3_quantity                1477
log_7_quantity                1477
log_15_quantity               1477
log_30_quantity               1477
log_total_quantity            1477
hot_1000_product              1477
hot_1600_product              1477
hot_10000_product             1477
hot_50000_product             1477
dtype: int64

3.4 数据清洗

train_data_normal = train_data[train_data.offset30_quantity <= train_data.offset_total_quantity]
train_data_normal[train_data_normal.offset_total_quantity < 0]
# drop null row
print(train_data_normal.isnull().sum())
train_data_valid = train_data_normal.dropna()
product_code                  0
category_id                   0
season                        0
offset3_amount_actual         0
offset7_amount_actual         0
offset15_amount_actual        0
offset30_amount_actual        0
offset_total_amount_actual    0
offset3_quantity              0
offset7_quantity              0
offset15_quantity             0
offset30_quantity             0
offset_total_quantity         0
log_3_quantity                0
log_7_quantity                0
log_15_quantity               0
log_30_quantity               0
log_total_quantity            0
hot_1000_product              0
hot_1600_product              0
hot_10000_product             0
hot_50000_product             0
dtype: int64
  1. 无序特征做onehot消除次序关系。
  2. 整理特征与类标。
  3. 循环生成预言家,让它预测,生成报告,然后把他丢到预言家数组里面(salesProphets)便于后面生成分析对比用的DataFrame。
# category_id, season onehot
feature_disordered = ['category_id', 'season']

feature_cols_3 = feature_disordered + ['offset3_quantity']
feature_cols_7 = feature_disordered + ['offset7_quantity']
feature_cols_15 = feature_disordered + ['offset15_quantity']
feature_cols_30 = feature_disordered + ['offset30_quantity']
feature_offsets = [feature_cols_3, feature_cols_7, feature_cols_15, feature_cols_30]

label_names = ['hot_1000_product', 'hot_1600_product', 'hot_10000_product', 'hot_50000_product']

algs = ['logistic', 'rf', 'svm', 'gdbt','xgboost']

salesProphets = []
for alg in algs:
    for feature in feature_offsets:
        for y in label_names:
            salesProphet = SalesProphet(train_data_valid, feature, y, alg)
            salesProphet.predict()
            salesProphet.genPredictReport()
            salesProphets.append(salesProphet)
    

预言家数组生成综合对比DataFrame

# 整理生成报告DataFrame
feature_column = []
label_column = []
model_alg_column = []
accuracy_column = []
f1_score_column = []
trainset_score_column = []
testset_score_column = []

for salesProphet in salesProphets:
    feature_column.append(salesProphet.features_name)
    label_column.append(salesProphet.label_name)
    model_alg_column.append(salesProphet.model_type)
    accuracy_column.append(salesProphet.accuracy_score)
    f1_score_column.append(salesProphet.f1_score)
    trainset_score_column.append(salesProphet.train_score)
    testset_score_column.append(salesProphet.test_score)

result_data = {'feature': feature_column, 'label': label_column, 
               'model_alg': model_alg_column, 'accuracy': accuracy_column, 
               'f1_score': f1_score_column, 'trainset_score': trainset_score_column, 
               'testset_score': testset_score_column}

result_df = DataFrame(result_data) 

分析报告保存

result_df.to_csv('data/result_df.csv')

结果

特征重要性分析

分数越高越重要

模型特征重要性分析

综合对比

分数越接近1越好

综合对比报告

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容