电网日现金流预测

平台支持

  1. Xshell可以在Windows界面下用来访问远端Linux系统下的服务器,连接后,可以运行远程机器的shell程序
  2. 浙江电网数据库https://yq.aliyun.com/articles/346262 ,应该是和阿里合作的一个大数据平台ODPS
  3. TensorFlow

1. 项目摘要

利用时间序列、回归、关联分析等大数据算法对2600万用户的历史用电行为、交费行为、资金到账规律进行分析、建模、预测,改变以往根据简单的统计分析和经验判断的粗线条管理模 式,提高预测数据的精准度,有效支撑日现金流量预算和融资计划安排。

2. 数据来源

本模型的训练数据和测试数据均来源于国家电网提供的时间跨度为2012年2月至2018年8月的杭州高压电各用户的缴费情况数据,包括缴费用户的行业分类,识别码,缴费方式,缴费时间, 到账时间和到账金额等各项指标。

3. 数据特征

  • 对于这6年的数据我们可以发现,其趋势长期来看明显升高,除了在2016年下半年有一个明显的下 降,这可能是由于G20在杭州举行所造成的
  • 然而对于6年2000多天的数据,我们并不能很好地看出其周期项,所以下面我们将每一年的数 据拆分开,分别分解研究以一年为周期和以一个月为周期的合理性。
  • 每一年(图中每一年均从8月份开始至第二年的7月份)从9月份开始的秋冬季节的日现金流较小,从3月份开始的春夏季节日现金流较大,而1月到二月由于春节长假会有一个 非常明显的低谷。2016至2017年的分解图中9月份的低谷也证实了G20期间日现金流的明显降低。 不仅如此,还能看出对于每一个月份都有较为明显的周期,月初月末较低,月中两个星期较高。

Arima模型

图片说明
图片说明

  • 以年为周期,构建月时间序列模型,用的是每月30天现金流平均值(该月收入总和/30),共有72个月,该序列有明显的周期(𝑇 = 12)。且趋势项也较为平缓,其一阶差分也通过了单位根检验,适合用ARIMA 序列对其进行拟合,我们 取前60个月的数据作为拟合数据,将后12个月的数据作为测试数据
  • 很明显月也是日现金流时间序列的一个周期,然而整个训练集有2160个样本,以日为单位进行时间序列拟合效果不佳,而且较为久远的数据对当下某一天的日 现金流值影响不太大,可能会使月平均值偏低,所以我们取5 月1 日至7月30日的修改后数据90个 样本作为训练集进行拟合。由于日数据波动较大,不太平稳,我们对其进行差分处理,其一阶差 分通过了单位根检验
  • 数据还有一个明显的周期――周,由于实际背景表明,周中工作日和周末休息日的电网日现金流有着较大的差异,周中的值在不是节假日等特殊情况下远大于周 末的值,所以周是我们不可忽视的周期。然而我们发现如果按照一个月30天的修改后数据并不能 被一周的天数7整除,而且按总值做时间序列预测在28天时误差较小,所以我们用上面处理好的一 个月28天并且带有周信息的数据,将每周的日均值计算出来得到一个新的序列(共有288个值)来 进行周时间序列模型的构建。
  • 日现金流周占比的回归:为了将以周为单位的总值分配到一周中的每一天,我们决定用回归分析
    的方法,分别建立一个月四周的日现金流回归模型,原理框架为:日现金流值= 月总值× 周比例× 日比例。所以,要用测试集数据回归出一周中每一天的占比,我们需要先选择测试集样本的因变量和自变量,并做相关处理。我们构建如下多元线性回归模型:
    𝑌1 = 𝑋𝛽 + 𝜀

GRU模型

1. 动因

目前关于预测售电数量的研究很多,而对于售电日现金流预测很少, 由于电费往往是按月缴费,并且企业是预先缴款,普通用户分为到期自动扣扣费和人为主动缴费,这也导致通过日售电量无法直接计算出日现金流。

2. 数据预处理

  • 去掉负数的电费值
    if money < 0:
        continue
    moneylist.append(money)
  • 归一化,归一化数据是梯度下降所需要的,防止梯度爆炸
    data_money[0] = sum(datedict[date]) / self.normal 

3.特征处理

1). data_hist特征

  • 将每笔金额组成的moneylist分桶,找到分桶点,其中k=3,即返回7个splitpoint,把数据分成加和相同的八段,代码如下:

      def div_list(self,inputlist, start, end, k):
          if end - start < 2:
              return [inputlist[0]]
          sortlist = inputlist[start:end]
          totalsum = sum(sortlist)
          presum = 0
          i = 0
          while (i < len(sortlist) and presum < totalsum // 2):
              presum += sortlist[i]
              i += 1
          if i >= len(sortlist):
              i -= 1
          if k > 1:
              l1 = self.div_list(inputlist, start, start + i, k - 1)
              l2 = self.div_list(inputlist, start + i, end, k - 1)
              lout = l1 + [inputlist[start + i]] + l2
          else:
              lout = [inputlist[start + i]]
          return lout
  • 对于特定第i天的收入金额流水,计算出各个桶中数据和所占当天总金额的比例

    temphist = self.gethist(datedict[date])
    for i in range(data_hist.shape[0]):
          data_hist[i] = temphist[i] / sum(datedict[date])
      def gethist(self,inputlist):
          outhist = [0] * (2 ** self.cut_points_num)
          for x in inputlist:
              index = (2 ** self.cut_points_num) - 1
              while (index > 0 and x < self.cutpoints[index - 1]):
                  index -= 1
    
              outhist[index] += x
          return outhist

    图片说明

    这里不用分位数点分割,是因为大多数据都很小,四分之三的数据都是量级为万的,而数据之间的差距是非常大的,最大的数据是千万级的。并且单个用户支付金额大的对现金流影响较大。


2). 时间特征

解析时间date形如20200101中的月和星期,得到它是一周中的星期几以及是哪个月的

date=str(date)
d1 = datetime.datetime.strptime(str(date), "%Y%m%d")  #strptime函数根据指定的格式把一个时间字符串解析为时间元组

month_index = d1.month
week_index = d1.weekday()
data_date[6 + month_index] = data_money[0]
data_date[week_index] = data_money[0]

因为春夏季节日现金流较大,秋冬季节的日现金流较小;工作日日现金流较大,节假日日现金流会明显减小。


3). 节假日特征

df_holiday = pd.read_csv(self.holidayfile)
dftemp = df_holiday[['date', 'isholiday']]
self.df_holiday = dftemp.set_index('date') 
data_holiday[0] = float(self.df_holiday.loc[int(date),'isholiday'])
train_dict[str(date)] = np.hstack([data_money, data_hist, data_date, data_holiday])

4. 整理数据

  • 把每个月的数据拉成31天,对于那些被创造出来的date,它的特征都为0

              date = '20120628'        #默认从这一天开始
              today = datetime.datetime.now()
              count = 1
              while date < str(today.year * 10000 + today.month * 100):
                  if date in train_dict.keys():
                      train_dict[date][-1] = -1
                  else:
                      train_dict[date] = [0] * self.feature_len
                      train_dict[date][-1] = -1
                      train_dict[date][-2] = 1
    
                  if date[6:8] < '31':
                      date = str(int(date) + 1)
                  elif date[4:6] < '12':
                      date = str(int(date[0:4]) * 100 + int(date[4:6]) + 1) + '01'
                  else:
                      date = str(int(date[0:4]) + 1) + '0101'
    
                  count += 1

5. 分离数据集

  • 测试集的数量为5个月,输入形状 (batch_size, timesteps, input_features) 为(3,35,30), 用35天的数据预测62天后的35天数据(因为电网系统4号能导出前一个月及这个月前四天的数据)

          train_x = self.data_x[:-self.test_len]     #第一行到倒数第test_len,2546-155=2391行
          train_y = self.data_x[:-self.test_len, 0]
          test_x = self.data_x[-self.test_len - self.cache_len:]     #31*5+4=159行
          test_y = self.data_x[-self.test_len - self.cache_len:, 0]
    
          epochs = 20
    
          #epochs = 100
          batch_size = 4
    
          train_len = train_x.shape[0] - self.gap_len - self.need_len  #2294
    
          test_input = np.zeros(shape=(3, self.need_len, self.feature_len), dtype=np.float)
          test_output = np.zeros(shape=(3, self.need_len, 1), dtype=np.float)

6. 生成器

        def get_train_batch():
            train_list = [x for x in range(train_len)]
            random.shuffle(train_list)                                  #用于将一个列表中的元素打乱
            train_list2 = [x for x in range(train_len // self.month_len)] * self.month_len  #0~73循环74遍
            random.shuffle(train_list2)
            train_batch_x = np.zeros(shape=(batch_size, self.need_len, self.feature_len), dtype=np.float)
            train_batch_y = np.zeros(shape=(batch_size, self.need_len, 1), dtype=np.float)
            i = 0
            k = 0
            while (i < (len(train_list) - batch_size)):              #将训练集切片
                for j in range(batch_size - 1):
                    train_batch_x[j] = train_x[train_list[i]:train_list[i] + self.need_len]
                    train_batch_y[j, :, 0] = train_y[train_list[i] + self.gap_len:train_list[i] + self.gap_len + self.need_len]
                    i += 1

                if k >= len(train_list2):
                    k = 0
                train_batch_x[-1] = train_x[train_list2[k]:train_list2[k] + self.need_len]
                train_batch_y[-1, :, 0] = train_y[train_list2[k] + self.gap_len:train_list2[k] + self.gap_len + self.need_len]
                k += 1
                yield train_batch_x, train_batch_y

7. 训练模型

  • 训练大中小三个gru模型

  • return_sequences:默认为false。当为false时,返回最后一层最后一个步长的hidden state;当为true时,返回最后一层的所有hidden state。

  • 三个模型的区别在于单元个数和循环层堆叠的个数

  • 设置了dropout以防过拟合

  • 最优化方式为adam

  • loss为mae

    def drnn_model_large(input_shape):
          input1 = Input(shape=input_shape)  # 40days
          input2 = GaussianNoise(1e-3)(input1)
          gru1 = GRU(32, return_sequences=True)(input2)
          bn1 = BatchNormalization()(gru1)
          act1 = Activation('relu')(bn1)
          dropout1 = Dropout(0.2)(act1)
          gru2 = GRU(16, return_sequences=True)(dropout1)
          bn2 = BatchNormalization()(gru2)
          act2 = Activation('relu')(bn2)
          dropout2 = Dropout(0.2)(act2)
          gru3 = GRU(8, return_sequences=True)(dropout2)
          bn2 = BatchNormalization()(gru3)
          act3 = Activation('relu')(bn2)
          output = Dense(1)(act3)
          out_model = Model(input1, output)
          return out_model
    
      @staticmethod
      def drnn_model_medium(input_shape):
          input1 = Input(shape=input_shape)  # 40days
          input2 = GaussianNoise(1e-3)(input1)
          gru1 = GRU(8, return_sequences=True)(input2)
          bn1 = BatchNormalization()(gru1)
          act1 = Activation('relu')(bn1)
          dropout1 = Dropout(0.2)(act1)
          gru2 = GRU(8, return_sequences=True)(dropout1)
          bn2 = BatchNormalization()(gru2)
          act2 = Activation('relu')(bn2)
          dropout2 = Dropout(0.2)(act2)
          gru3 = GRU(4, return_sequences=True)(dropout2)
          bn2 = BatchNormalization()(gru3)
          act3 = Activation('relu')(bn2)
          output = Dense(1)(act3)
          out_model = Model(input1, output)
          return out_model
    
    
@staticmethod
def drnn_model_small(input_shape):
    input1 = Input(shape=input_shape)  # 40days
    input2 = GaussianNoise(1e-3)(input1)
    gru1 = GRU(32, return_sequences=True)(input2)
    bn1 = BatchNormalization()(gru1)
    act1 = Activation('relu')(bn1)
    dropout1 = Dropout(0.2)(act1)
    gru2 = GRU(16, return_sequences=True)(dropout1)
    bn2 = BatchNormalization()(gru2)
    act3 = Activation('relu')(bn2)
    output = Dense(1)(act3)
    out_model = Model(input1, output)
    return out_model

8. 对比结果

  • 基于GRU单元循环神经网络来预测电力部门日现金流的建模方法,构建的循环神经网络模型简单实用,相比ARIMA模型有更少的人工干预,且日相对误差、平均绝对百分比误差以及标准误差三项指标的结果都比ARIMA更小,即预测精度更高,结果更加鲁棒
相对误差    1日    2日    3日    4日    5日    6日    7日    8日
ARIMA    1.18    0.45    1.13    0.52    1.69    8.00    4.96    0.75
本文模型    0.94    0.24    0.30    0.21    0.36    0.01    0.56    0.02
相对误差    9日    10日    11日    12日    13日    14日    15日    16日
ARIMA    0.28    0.83    0.40    0.40    0.01    8.12    0.32    0.70
本文模型    0.13    0.21    0.37    0.04    0.33    0.14    0.83    0.05
相对误差    17日    18日    19日    20日    21日    22日    23日    24日
ARIMA    0.97    0.83    0.54    0.40    2.49    16.19    0.52    0.66
本文模型    0.04    0.46    0.21    0.03    0.22    0.06    0.13    0.33
相对误差    25日    26日    27日    28日    29日    30日    
ARIMA    0.83    0.07    0.14    0.07    26.60    4.15    
本文模型    0.14    0.14    0.09    0.08    0.78    0.14    

    MAPE    RMSE
ARIMA    281%    129724725.9
本文模型    25%    27123380.09
全部评论

相关推荐

点赞 评论 收藏
转发
头像
不愿透露姓名的神秘牛友
03-20 10:18
点赞 评论 收藏
转发
点赞 1 评论
分享
牛客网
牛客企业服务