Introducción
En el sistema de datos, a menudo es necesario contar algunos datos de duración, como el tiempo en línea. Algunos de estos datos son mejores para las estadísticas, mientras que otros son un poco más problemáticos. Por ejemplo, las estadísticas sobre el tiempo en línea del usuario basadas en los registros de inicio y cierre de sesión.
Podemos usar las funciones de ventana adelantar y retrasar para completarlo, lo cual es muy conveniente. La función de adelantar es empalmar los datos de la enésima fila después de una cierta columna de datos a la fila actual, y la demora es empalmar los datos de la enésima fila en frente de la columna especificada a la fila actual.
lag(column,n,default)
lead(column,n,default)
La columna de parámetro es para seleccionar la columna que se empalmará, el parámetro n indica cuántas filas se moverán, generalmente se mueve una fila, el valor predeterminado es el valor predeterminado, si no hay una fila antes de la demora y no hay una fila después de la salida, se usa el valor predeterminado.
Los puntos clave del uso de estas 2 funciones son: particionar y ordenar
select gid,
lag(time,1,'0') over (partition by gid order by time) as lag_time,
lead(time,1,'0') over (partition by gid order by time) as lead_time
from table_name;
La partición es agrupación. Utilice la partición por para agrupar varias columnas con comas.
La clasificación se especifica por orden y varias columnas de clasificación están separadas por comas
La combinación de adelanto y retraso puede funcionar más allá de nuestra imaginación.
Por ejemplo, para realizar estadísticas de tiempo en línea a través de los registros de inicio de sesión y cierre de sesión, si los requisitos no son altos, sencillo: agrupación de ID de usuario, tiempo ascendente y luego usar plomo para hacer que el próximo tiempo de cierre de sesión empalmado con la línea de tiempo de inicio de sesión actual se pueda calcular fácilmente.
Pero teniendo en cuenta que hay problemas entre días y pérdida de registros, no estoy seguro de que el primero sea el registro de inicio de sesión y el segundo sea el registro de salida.
Al combinar el adelanto y el retraso, podemos filtrar fácilmente los datos ilegales.
Código específico
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Encoders;
import org.apache.spark.sql.Row;
import org.apache.spark.sql.SparkSession;
import org.apache.spark.sql.api.java.UDF6;
import org.apache.spark.sql.functions;
import org.apache.spark.sql.types.DataTypes;
import org.junit.Before;
import org.junit.Test;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.LinkedList;
import java.util.List;
public class SparkLoginTimeTest implements Serializable {
private SparkSession sparkSession;
@Before
public void setUp() {
sparkSession = SparkSession
.builder()
.appName("test")
.master("local")
.getOrCreate();
}
private static List<Info> getInfos() {
String[] gids = {
"10001","10001","10002","10002","10003","10003","10004","10004","10005","10005"};
LocalDateTime base = LocalDateTime.of(2020, 1, 1,0,0,0);
LinkedList<Info> infos = new LinkedList<>();
for(int i=0;i<50;i++){
Info info = new Info();
info.setGid(gids[i%10]);
info.setResult(i % 2);
info.setDate(base.plus(i * 5, ChronoUnit.MINUTES).toInstant(ZoneOffset.UTC).toEpochMilli());
infos.add(info);
}
return infos;
}
@Test
public void lag(){
List<Info> infos = getInfos();
sparkSession.udf().register("accTimes",accTimes(), DataTypes.LongType);
Dataset<Info> dataset = sparkSession.createDataset(infos, Encoders.bean(Info.class));
dataset.show(100);
dataset.createOrReplaceTempView("temp");
String sql = "select gid,result,date," +
"lead(date,1,-1) over(partition by gid order by date) lead_date," +
"lead(result,1,-1) over(partition by gid order by date) lead_result," +
"lag(result,1,-1) over(partition by gid order by date) lag_result," +
"lag(date,1,-1) over(partition by gid order by date) lag_date" +
" from temp";
Dataset<Row> baseDs = sparkSession.sql(sql);
Dataset<Row> rs = baseDs.withColumn("acc_times",
functions.callUDF("accTimes",
baseDs.col("result"),
baseDs.col("date"),
baseDs.col("lead_result"),
baseDs.col("lead_date"),
baseDs.col("lag_result"),
baseDs.col("lag_date")
)).groupBy("gid")
.agg(functions.sum("acc_times").alias("accTimes")).na().fill(0)
.select("gid", "accTimes");
rs.show(100);
}
private static UDF6<Integer,Long,Integer,Long,Integer,Long,Long> accTimes(){
return new UDF6<Integer, Long, Integer, Long, Integer, Long, Long>() {
long dayMill = 86400000;
@Override
public Long call(Integer result, Long time, Integer headResult, Long headTime, Integer lagResult, Long lagTime) {
if(lagResult == -1){
//第一行
if(result == 1){
//退出,计算退出到这一天的开始时间
return time - (time / dayMill) * dayMill ;
}
}
if(headResult == -1){
//最后一行
if(result == 0){
//进入,计算到这一天结束
return (time / dayMill + 1) * dayMill - time;
}
}
if(result == 0 && headResult == 1){
//当前行是进入,并且下移行是退出
long rs;
rs = headTime - time;
if(rs > 0) {
return rs;
}
}
return 0L;
}
};
}
public static class Info implements Serializable {
/**
* 用户唯一标识
*/
private String gid;
/**
* 登录、退出时间
*/
private Long date;
/**
* 0-登录、1-退出
*/
private Integer result;
public Integer getResult() {
return result;
}
public void setResult(Integer result) {
this.result = result;
}
public String getGid() {
return gid;
}
public void setGid(String gid) {
this.gid = gid;
}
public Long getDate() {
return date;
}
public void setDate(Long date) {
this.date = date;
}
}
}