如何计算"自定义运行总计"在spark 1.5数据帧中

时间:2015-12-15 18:07:39

标签: python scala apache-spark dataframe apache-spark-sql

我有一个存储在镶木地板文件中的每个LoanId的贷款支付历史记录,并尝试计算"过期"每笔贷款的每个期间的金额。 这将是简单的分区窗口任务,如果不是如何计算到期金额的棘手性质。

如果客户支付的金额低于到期金额,则过期支付额增加,另一方面,如果客户预付款,则在后续期间忽略额外支付(下面样本中的第5行和第6行)。

LoanID  Period  DueAmt  ActualPmt   PastDue
1       1       100     100             0
1       2       100     60              -40
1       3       100     100             -40
1       4       100     200             0   <== This advance payment is not rolled to next period
1       5       100     110             0   <== This advance payment is not rolled to next period
1       6       100     80              -20
1       7       100     60              -60
1       8       100     100             -60
2       1       150     150             0
2       2       150     150             0
2       3       150     150             0
3       1       200     200             0
3       2       200     120             -80
3       3       200     120             -160

要解决这个问题,我实际上需要为按周期排序的每个分区(LoanID)应用自定义函数。

spark中有哪些选项。

直接但复杂似乎使用DF-&gt; RDD-&GT; groupby,将lambda convert应用回dataframe。

使用 window 功能,更优雅的是自定义 UDAF (在scala?中),但无法找到此单个实现示例。

好的,所以我尝试了第一个解决方案,从Dataframe转发到配对RDD并返回

    from pyspark.sql import Row 
    def dueAmt(partition):
        '''
        @type partition:list 
        '''
        #first sort rows
        sp=sorted(partition, key=lambda r: r.Period )
        res=[]
        due=0
        for r in sp:
            due+=r.ActualPmt-r.DueAmt
            if due>0: due=0;
            #row is immutable so we need to create new row with updated value
            d=r.asDict()
            d['CalcDueAmt']=-due
            newRow=Row(**d)
            res.append(newRow)
        return res    

    df = sqlContext.read.format('com.databricks.spark.csv').options(header='true', inferschema='true').load('PmtDueSample2.csv').cache()
    rd1=df.rdd.map(lambda r: (r.LoanID, r ) )
    rd2=rd1.groupByKey()
    rd3=rd2.mapValues(dueAmt)
    rd4=rd3.flatMap(lambda t: t[1] )
    df2=rd4.toDF()

似乎工作。

在这次旅程中,我实际上发现了pyspark实现中的一些错误。

  1. 在类Row中实现____call____是错误的。
  2. 该Row的构造函数中的烦人的bug。没有明显的理由____new____分类 列,所以在旅程结束时,我得到的表有列 按字母顺序排序。这简直太难看了 最终结果。

1 个答案:

答案 0 :(得分:0)

既不漂亮也不高效,但应该给你一些工作。让我们从创建和注册表开始:

val df = sc.parallelize(Seq(
  (1, 1, 100, 100), (1, 2, 100, 60), (1, 3, 100, 100),
  (1, 4, 100, 200), (1, 5, 100, 110), (1, 6, 100, 80),
  (1, 7, 100, 60), (1, 8, 100, 100), (2, 1, 150, 150),
  (2, 2, 150, 150), (2, 3, 150, 150), (3, 1, 200, 200),
  (3, 2, 200, 120), (3, 3, 200, 120)
)).toDF("LoanID", "Period", "DueAmt", "ActualPmt")

df.registerTempTable("df")

接下来让我们定义并注册一个UDF:

case class Record(period: Int, dueAmt: Int, actualPmt: Int, pastDue: Int)

def runningPastDue(idxs: Seq[Int], dues: Seq[Int], pmts: Seq[Int]) = {
  def f(acc: List[(Int, Int, Int, Int)], x: (Int, (Int, Int))) = 
    (acc.head, x) match {
      case ((_, _, _, pastDue), (idx, (due, pmt))) => 
        (idx, due, pmt, (pmt - due + pastDue).min(0)) :: acc
    }

  idxs.zip(dues.zip(pmts))
    .toList
    .sortBy(_._1)
    .foldLeft(List((0, 0, 0, 0)))(f)
    .reverse
    .tail
    .map{ case (i, due, pmt, past) => Record(i, due, pmt, past) }
}

sqlContext.udf.register("runningPastDue", runningPastDue _)

聚合和计算总和:

val aggregated = sqlContext.sql("""
  SELECT LoanID, explode(pmts) pmts FROM (
    SELECT LoanId, 
           runningPastDue(
             collect_list(Period), 
             collect_list(DueAmt), 
             collect_list(ActualPmt)
           ) pmts
    FROM df GROUP BY LoanID) tmp""")

val flattenExprs = List("Period", "DueAmt", "ActualPmt", "PastDue")
  .zipWithIndex
  .map{case (c, i) => col(s"tmp._${i+1}").alias(c)}

最后压扁:

val result = aggregated.select($"LoanID" :: flattenExprs: _*)