DataFrame新增历史同期特征时赋值出现NaN问题求助
Pandas生成上一周均值特征时全为NaN的解决方法
问题场景
我有一个包含num_posts和date字段的Pandas DataFrame,想要生成prev_week_of_last_year、prev_month_of_last_year这类时间相关特征。已经实现了create_calendar_columns、calculate_curr_week_year_avg函数计算周均值、年均值,但调用calculate_prev_1w_curr_year函数给新列赋值时,结果全是NaN。
原始代码与数据
import pandas as pd import numpy as np # 创建示例DataFrame df = pd.DataFrame({'num_posts': [4, 6, 3, 9, 1, 14, 2, 5, 7, 2,12,1,2,3], 'date' : ['2020-03-01', '2020-01-02', '2020-01-03', '2020-01-04', '2019-01-05', '2019-01-06', '2020-01-07', '2020-01-08', '2020-01-09', '2020-01-10','2020-01-11','2019-01-10','2019-01-08','2019-01-09']}) cols=['num_posts'] # 相关函数定义 def create_calendar_columns(df,dt): df[dt]=pd.to_datetime(df[dt]) df['day_number']= pd.to_datetime(df[dt]).dt.dayofyear df['week_number']= pd.to_datetime(df[dt]).dt.isocalendar().week df['year']= pd.to_datetime(df[dt]).dt.year return df def calculate_curr_week_year_avg(df,cols,dt): df=create_calendar_columns(df,dt) for col in cols: df['week_avg_'+str(col)]= df.groupby(['year','week_number'])[col].transform(np.mean) df['year_avg_'+str(col)]= df.groupby(['year'])[col].transform(np.mean) return df def calculate_prev_1w_curr_year(df,cols,dt): for col in cols: df['prev_1w_curr_year_'+str(col)]=df[(df['week_number']==(df['week_number']-1)) & (df['year']==df['year'])]['week_avg_'+str(col)] return df df=calculate_curr_week_year_avg(df,cols,'date') df=calculate_prev_1w_curr_year(df,cols,'date')
中间运行结果(调用calculate_curr_week_year_avg后)
| num_posts | date | day_number | week_number | year | week_avg_num_posts | year_avg_num_posts | |
|---|---|---|---|---|---|---|---|
| 0 | 4 | 2020-03-01 | 61 | 9 | 2020 | 4.000 | 5.556 |
| 1 | 6 | 2020-01-02 | 2 | 1 | 2020 | 6.000 | 5.556 |
| 2 | 3 | 2020-01-03 | 3 | 1 | 2020 | 6.000 | 5.556 |
| 3 | 9 | 2020-01-04 | 4 | 1 | 2020 | 6.000 | 5.556 |
| 4 | 1 | 2019-01-05 | 5 | 1 | 2019 | 7.500 | 4.200 |
| 5 | 14 | 2019-01-06 | 6 | 1 | 2019 | 7.500 | 4.200 |
| 6 | 2 | 2020-01-07 | 7 | 2 | 2020 | 5.600 | 5.556 |
| 7 | 5 | 2020-01-08 | 8 | 2 | 2020 | 5.600 | 5.556 |
| 8 | 7 | 2020-01-09 | 9 | 2 | 2020 | 5.600 | 5.556 |
| 9 | 2 | 2020-01-10 | 10 | 2 | 2020 | 5.600 | 5.556 |
| 10 | 12 | 2020-01-11 | 11 | 2 | 2020 | 5.600 | 5.556 |
| 11 | 1 | 2019-01-10 | 10 | 2 | 2019 | 2.000 | 4.200 |
| 12 | 2 | 2019-01-08 | 8 | 2 | 2019 | 2.000 | 4.200 |
| 13 | 3 | 2019-01-09 | 9 | 2 | 2019 | 2.000 | 4.200 |
错误原因
calculate_prev_1w_curr_year里的筛选逻辑完全错误:
df['week_number'] == (df['week_number'] - 1)是逐行判断,同一行的周数不可能等于自身减1,这个条件永远不成立,筛选结果是空的Series,赋值后自然全为NaN。df['year'] == df['year']是恒成立的冗余条件,没有实际作用。
解决方法
正确思路是:先建立年份-周数到周均值的映射,再为每一行匹配同一年、周数减1对应的均值。下面提供两种可靠实现方式:
方式一:使用Merge匹配映射表
def calculate_prev_1w_curr_year(df, cols, dt): # 提取唯一的(year, week_number)对应的周均值,构建映射表 week_avg_ref = df.drop_duplicates(subset=['year', 'week_number'])[['year', 'week_number', 'week_avg_num_posts']] # 遍历列,匹配上一周均值 for col in cols: # 把映射表的周数减1,作为匹配键 week_avg_ref['match_week'] = week_avg_ref['week_number'] - 1 # 合并到原表,匹配同一年、目标周=原表周数的记录 df = df.merge( week_avg_ref[['year', 'match_week', 'week_avg_num_posts']], left_on=['year', 'week_number'], right_on=['year', 'match_week'], how='left', suffixes=('', '_prev') ) # 重命名结果列并清理临时字段 df.rename(columns={'week_avg_num_posts_prev': f'prev_1w_curr_year_{col}'}, inplace=True) df.drop('match_week', axis=1, inplace=True) return df
方式二:按年份分组后Shift
这种方式更简洁,利用Pandas的groupby和shift能力:
def calculate_prev_1w_curr_year(df, cols, dt): for col in cols: # 按年份分组,先按周数排序,再对周均值列进行shift,得到上一周的均值 df[f'prev_1w_curr_year_{col}'] = df.groupby('year').apply( lambda group: group.sort_values('week_number')[f'week_avg_{col}'].shift(1) ).reset_index(level=0, drop=True) return df
修正后运行结果
调用修正后的函数后,新列prev_1w_curr_year_num_posts会正确生成:
| num_posts | date | day_number | week_number | year | week_avg_num_posts | year_avg_num_posts | prev_1w_curr_year_num_posts | |
|---|---|---|---|---|---|---|---|---|
| 0 | 4 | 2020-03-01 | 61 | 9 | 2020 | 4.000 | 5.556 | NaN |
| 1 | 6 | 2020-01-02 | 2 | 1 | 2020 | 6.000 | 5.556 | NaN |
| 2 | 3 | 2020-01-03 | 3 | 1 | 2020 | 6.000 | 5.556 | NaN |
| 3 | 9 | 2020-01-04 | 4 | 1 | 2020 | 6.000 | 5.556 | NaN |
| 4 | 1 | 2019-01-05 | 5 | 1 | 2019 | 7.500 | 4.200 | NaN |
| 5 | 14 | 2019-01-06 | 6 | 1 | 2019 | 7.500 | 4.200 | NaN |
| 6 | 2 | 2020-01-07 | 7 | 2 | 2020 | 5.600 | 5.556 | 6.000 |
| 7 | 5 | 2020-01-08 | 8 | 2 | 2020 | 5.600 | 5.556 | 6.000 |
| 8 | 7 | 2020-01-09 | 9 | 2 | 2020 | 5.600 | 5.556 | 6.000 |
| 9 | 2 | 2020-01-10 | 10 | 2 | 2020 | 5.600 | 5.556 | 6.000 |
| 10 | 12 | 2020-01-11 | 11 | 2 | 2020 | 5.600 | 5.556 | 6.000 |
| 11 | 1 | 2019-01-10 | 10 | 2 | 2019 | 2.000 | 4.200 | 7.500 |
| 12 | 2 | 2019-01-08 | 8 | 2 | 2019 | 2.000 | 4.200 | 7.500 |
| 13 | 3 | 2019-01-09 | 9 | 2 | 2019 | 2.000 | 4.200 | 7.500 |
注:周数为1的行没有上一周数据,所以对应值为NaN,这是合理的。
内容的提问来源于stack exchange,提问作者user3585510
相关产品推荐
相关产品推荐

