【问题标题】:Best way to get 'selling windows' for each product category in pandas?为熊猫中的每个产品类别获取“销售窗口”的最佳方式?
【发布时间】:2020-08-20 21:49:45
【问题描述】:

所以我的数据框包含许多产品多年的销售详情,图表如下所示:

我正在尝试找出每种产品的销售窗口。

到目前为止我尝试过的:

我想到的方法是获取每年六个月间隔的最小、中值和最大日期值,并将(最小到中值)声明为最差销售期,将中值到最大声明为该产品的最佳销售窗口。我现在使用了六个月的代码,但也想在一年内获得它。哪个效果最好:

def dater(date):
    print(date)
    if type(date)==float:
        return '-'
    months = ['','Jan', 'Feb', 'Mar', 'Apr', 'May','Jun', 'Jul', 'Aug','Sep', 'Oct', 'Nov', 'Dec']
    period = ['Start', 'Mid', 'End','End']
    return months[date.month]+' '+period[date.day//10]


def grpRes(grp):
    return pd.Series([grp.Date.min(), grp.Date.max(), grp.Amount.mean()],
        index=['start', 'end', 'value'])


best_windows = pd.DataFrame(columns = df.select_dtypes(exclude='object').columns)
for col in df.select_dtypes(exclude='object').columns:
    for year in ['2017', '2018', '2019', '2020']:
        print(f'For year {year} and category {col}')
        temp = df.loc[year,col][df[col]>=df[col].quantile(0.7)]
        print('temp created')
        if len(temp)>0:
            du = temp.reset_index().rename(columns = {'order_start_date': 'Date', col:'Amount'})
            res = du.groupby(du.Date.diff().dt.days.fillna(1, downcast='infer')
                .gt(20).cumsum()).apply(grpRes)
            res.index.name = 'chunk'
            for row in res.iterrows():
                print(row)
                best_windows.loc[year+' Window: '+str(row[0]+1)+' start',col] = row[1].start.date().strftime('%d-%m-%Y')

然后,我根据所有年份的值将窗口定义为窗口的起始范围和结束范围。但似乎是一种可怕的方法。这虽然给了我不同年份的日期范围如下:

    2017 Window: 1 end  2017 Window: 1 start    2017 Window: 2 end  2017 Window: 2 start    2018 Window: 1 end  2018 Window: 1 start    2018 Window: 2 end  2018 Window: 2 start    2018 Window: 3 end  2018 Window: 3 start    2019 Window: 1 end  2019 Window: 1 start    2019 Window: 2 end  2019 Window: 2 start    2019 Window: 3 end  2019 Window: 3 start    2020 Window: 1 end  2020 Window: 1 start    2020 Window: 2 end  2020 Window: 2 start    2020 Window: 3 end  2020 Window: 3 start    2020 Window: 4 end  2020 Window: 4 start
B                                           31-12-2019  08-11-2019                  09-01-2020  01-01-2020  31-07-2020  11-02-2020              
D   12-06-2017  13-05-2017  14-10-2017  16-08-2017  13-06-2018  24-05-2018  20-08-2018  11-07-2018  03-11-2018  27-09-2018  10-11-2019  22-10-2019  31-12-2019  28-12-2019          31-07-2020  01-01-2020                      
H                   06-04-2018  23-03-2018  09-08-2018  27-06-2018  16-11-2018  02-11-2018  25-05-2019  21-04-2019  15-08-2019  12-07-2019  31-12-2019  30-10-2019  31-07-2020  01-01-2020                      
J   12-02-2017  15-01-2017  31-12-2017  25-10-2017  11-02-2018  01-01-2018  31-12-2018  12-10-2018          24-02-2019  01-01-2019  31-12-2019  10-10-2019          04-02-2020  01-01-2020                      
L                   08-11-2018  03-11-2018  31-12-2018  06-12-2018          07-03-2019  01-01-2019  01-05-2019  24-04-2019  31-12-2019  02-09-2019  06-03-2020  01-01-2020  19-04-2020  10-04-2020  14-05-2020  10-05-2020  31-07-2020  26-07-2020
LO  31-12-2017  06-09-2017          03-01-2018  01-01-2018  31-12-2018  23-09-2018          10-02-2019  01-01-2019  31-12-2019  25-09-2019          11-02-2020  01-01-2020                      
M   11-09-2017  15-01-2017          15-10-2018  03-07-2018                  02-05-2019  22-04-2019  24-11-2019  18-11-2019          13-05-2020  28-03-2020  23-07-2020  21-06-2020              
P   03-05-2017  21-01-2017  19-10-2017  11-08-2017  23-04-2018  31-01-2018  10-10-2018  02-08-2018          23-04-2019  23-02-2019  06-10-2019  04-09-2019          04-04-2020  29-02-2020                      
S   26-07-2017  24-03-2017          01-07-2018  25-03-2018                  01-05-2019  18-04-2019  10-08-2019  23-05-2019          31-07-2020  01-04-2020                      
SH  12-08-2017  07-05-2017          11-08-2018  05-05-2018                  10-08-2019  01-05-2019                  31-07-2020  29-04-2020                      
SK                                          31-12-2019  12-12-2019                  01-01-2020  01-01-2020  31-07-2020  24-05-2020              
SKO 26-09-2017  01-05-2017          19-09-2018  03-05-2018                  25-07-2019  09-07-2019                  31-07-2020  04-05-2020                      
SL  10-06-2017  24-05-2017          06-05-2018  06-05-2018  16-07-2018  31-05-2018          01-08-2019  12-03-2019                  31-07-2020  16-02-2020                      
U                                           17-05-2019  18-04-2019  24-06-2019  10-06-2019          01-06-2020  27-03-2020  31-07-2020  25-06-2020              
V   13-02-2017  15-01-2017  31-12-2017  14-09-2017  05-03-2018  01-01-2018  31-12-2018  25-09-2018          19-02-2019  01-01-2019  31-12-2019  22-10-2019          22-01-2020  01-01-2020  

                

现在我可以使用我编写的日期函​​数将其转换为月份和精确的月份窗口:

best_windows = best_windows.transpose().applymap(dater)

但这给了我一个年度解决方案,而不是一个单一的销售窗口。

理想情况下我想要达到的目标:

一年中每种产品的最佳销售窗口和最差销售窗口,我可以说,嘿,在一年中的这个时候,这个产品很受欢迎(例如,产品 A 在 3 月底到 6 月中旬销售最好)大致由图中显示的销售百分比曲线的波峰/波谷,理想情况下还有过渡期,以便更好地了解每种产品的销售窗口。

数据样本:

我的数据如下所示。请注意,这些是基于每个类别所代表的总销售额的 %s。我说的百分比是指总销售额的百分比。假设总销售额为 10 美元。其中产品 A 售价 5 美元,B 售价 3 美元,C 售价 2 美元。然后 % 值为:A= 50%,B=30%,C=20%。当然,这仅在我尝试添加一整年数据的多个产品时才有效,因为它可以更好地解释我的数据中的季节性,而这在较小的样本中是无法检测到的。

链接:https://pern-my.sharepoint.com/:x:/g/personal/syed_8911255_talmeez_pk/EY_w794N49dGgWfYal90ZLUBt5TDB3asJEayuJHD1QdRog?e=Ih4Wo8

【问题讨论】:

  • 数据样本会很有帮助!
  • 我添加了一个不错的大数据样本
  • @piRSquared 也许你可以帮忙。看到您对股票时间序列的出色回答,感觉您可能会有所帮助

标签: python pandas analytics


【解决方案1】:

这样的事情怎么样:

# usng sin to generate seasonal data
period = 365 * 4
dates = pd.date_range('2016-01-01', periods=period)

np.random.seed(42)
pure = np.sin(np.linspace(6, 30, period))
noise = np.random.normal(0, 1, period)
signal = pure + 20 + noise

df = pd.DataFrame({'date': dates, 'signal': signal}).set_index('date')
df['smoothed'] = df['signal'].rolling(30).mean()

# get best/worst selling months
# rolling max/min method
threshold = 0.97
window = 320
df['best'] = df['smoothed'].where( df['smoothed'] > df['smoothed'].rolling(window).max() * threshold, other=np.nan)
df['worst'] = df['smoothed'].where( df['smoothed'] < df['smoothed'].rolling(window).min() / threshold, other=np.nan)
df.iloc[365:, 1:].plot(figsize=(14,10))

滚动最大/最小位并不完美,但如果每年的最大/分钟每年都有显着变化,则它是必要的。您还必须使用这种方法忽略第一年的数据。

下一个方法通过首先单独提取年度最大值/最小值来解决这些问题:

# annual max/min method
threshold = 0.97
df['max'], df['min'] = df['smoothed'].max(), df['smoothed'].min()
df['best'] = df['smoothed'].where( df['smoothed'] > df['max'] * threshold, other=np.nan)
df['worst'] = df['smoothed'].where( df['smoothed'] < df['min'] / threshold, other=np.nan)
df.iloc[365:, 1:-2].plot(figsize=(14,10))

【讨论】:

  • 嘿@footfalcon 我已经将它们转换为销售额百分比,所以我认为滚动最小值/最大值也应该有效?还是我应该采用保持原始值的年度方法?
  • 最好使用年度而不是滚动方法并使用未调整的数据,但可以尝试一下。此外,如果您正在寻找季节性,则不应使用 % change(如果这就是您所说的 % sales)。例如,如果我上面的虚拟数据是温度,那么炎热夏季月份的同比变化百分比与冬季月份的年度变化百分比没有太大差异。也就是说,如果您的数据具有年度季节性,则计算年度百分比变化会从数据中移除该季节性。
  • 不,我指的是总销售额百分比。假设总销售额为 10 美元。其中产品 A 售价 5 美元,B 售价 3 美元,C 售价 2 美元。然后 % 值为:A= 50%,B=30%,C=20%。这当然只有在有多个产品时才有效
  • 对于非 % 销售它不起作用。对于%的销售额,我试图让它发挥作用。看看当我尝试非百分比销售额时会发生什么:ibb.co/HhnqP3x
【解决方案2】:

我认为这里首先要考虑的是您是想要静态模型还是想要一种自我更新的模型。

我的建议是使用静态模型,因为使用到目前为止积累的所有数据来获得产品的最畅销和最差销售窗口,并将其作为下一年的推荐。发布您可以再次更新您的推荐。

接下来,您需要决定您想称其为好与坏。可能是这样,前 20 个百分点是好的销售百分比,而后 20 个百分点是坏的。我们将此阈值称为 T 百分位数。

现在回到主要部分,因此您的假设是,当产品的销售额百分比高(高于 T)或低(低于 T)时,每年都有固定的窗口。 因此,首先我们需要获取一年中每一天的平均值(您也可以拟合回归模型而不是求平均值,这样可以使事情变得平滑并使您的预测更加稳健)。

df["datetime"] = pd.to_datetime(df["datetime"])
df["dayofyear"] = df["datetime"].dt.dayofyear
df["year"] = df["datetime"].dt.year

dfg = df.groupby("dayofyear").mean()

然后,当平均/预测销售曲线与 T 百分位相交时,我们开始该区间,并在它再次相交时停止。

def get_thresh_crossing_intervals(arr):
    crossings = np.diff(np.sign(arr))
    # You might also want to wrap arrays to cover spans around end of year
    ends = np.where(crossings == -2)[0]
    starts = np.where(crossings == 2)[0][:len(ends)]  
    return list(zip(starts, ends))


def post_process_intervals(intervals):
    return [(p, q) for p, q in intervals if q-p>=7]


def get_col_intervals(df, col, top_thresh=0.2, bot_thresh=0.2):
    # Get quantile based thresholds
    top_qnt = df[col].quantile(1 - top_thresh)
    bot_qnt = df[col].quantile(bot_thresh)
    
    # Make threshold as zero line
    top_df = df[col] - top_qnt 
    bot_df = df[col] - bot_qnt
    
    # Get top crossings and intervals
    top_intervals = get_thresh_crossing_intervals(top_df)
    bot_intervals = get_thresh_crossing_intervals(bot_df)
    
    # Some post processings (e.g. only keep intervals with more than a week)
    top_intervals = post_process_intervals(top_intervals)
    bot_intervals = post_process_intervals(bot_intervals)
    
    return {'top_intervals': top_intervals, 'bot_intervals': bot_intervals}

product_intervals = {}
for col in ["A", "B"]:
    product_intervals[col] = get_col_intervals(dfg, col)


product_intervals
Sample Output:
{'A': {'top_intervals': [(15, 60), (117, 136)],
  'bot_intervals': [(1, 85), (103, 236), (273, 286), (287, 320)]},
 'B': {'top_intervals': [(120, 140), (198, 209), (306, 339)],
  'bot_intervals': [(36, 61), (80, 262)]}}

此外,我们只保留超过一定长度的区间,否则我们将其删除,或者我们可以按区间的长度/曲线下面积对区间进行排序并保留前 n 个。

注意: 如果您的时间序列不是静止的,则需要先使它们静止(只需检查产品的销售额百分比是否逐年发生显着变化)

【讨论】:

  • 我的意思是占总销售额的百分比。假设总销售额为 10 美元。其中产品 A 售价 5 美元,B 售价 3 美元,C 售价 2 美元。然后 % 值为:A= 50%,B=30%,C=20%。此百分比是针对每天计算的,因此可能不会有很大差异。同比变化很大,但使用百分比销售额抵消了。 (据我所知)
  • 并更新了我的尝试。到目前为止,我已经完成了详细的代码,是的,我将阈值提高到了 30%。我也更喜欢静态模型,但正如您所见,不同年份的窗口甚至关闭也不同
  • 如果windows真的那么不同,那么逐年计算它们可能没有用,因为它不会告诉我们关于明年的太多信息。那么我们怎么能说这种产品在每年的这个时候卖得很好呢?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-08-18
相关资源
最近更新 更多