You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spark Dataset按距离条件分组:合并5公里内城市人口

解决方案

1. 把loc列转成整数元组

假设你的loc列是类似"(123,456)"的字符串格式,两种方法搞定:

方法1:用Spark内置函数拆分(推荐)

直接拆成纬度、经度两列,后续计算距离更方便,要元组的话再组合回去:

import org.apache.spark.sql.functions._

// 先清理括号、拆分坐标转整数
val dfWithCoords = originalDF
  .withColumn("loc_clean", regexp_replace(col("loc"), "[()]", ""))
  .withColumn("lat", split(col("loc_clean"), ",")(0).cast("int"))
  .withColumn("lon", split(col("loc_clean"), ",")(1).cast("int"))
  .drop("loc_clean")

// 如果必须要整数元组格式,用struct组合
val dfWithTuple = dfWithCoords.withColumn("loc_tuple", struct(col("lat"), col("lon")))

方法2:自定义UDF(适合复杂格式)

如果loc格式不规整,写个UDF解析更灵活:

import org.apache.spark.sql.functions.udf

val parseLocToTuple = udf((locStr: String) => {
  val coords = locStr.replaceAll("[()]", "").split(",")
  (coords(0).toInt, coords(1).toInt)
})

val dfWithTuple = originalDF.withColumn("loc_tuple", parseLocToTuple(col("loc")))

2. 5公里内城市分组汇总人口

直接用groupBy搞不定这个需求,因为得先算两两城市的距离,再把近的归为一组,用「自连接+Haversine距离+连通分量标记」的思路实现:

步骤1:写个Haversine距离计算函数

用来算地球上两点的球面距离,输出单位是公里:

import org.apache.spark.sql.functions.udf

val haversineDistance = udf((lat1: Int, lon1: Int, lat2: Int, lon2: Int) => {
  val earthRadius = 6371 // 地球半径(公里)
  val dLat = Math.toRadians(lat2 - lat1)
  val dLon = Math.toRadians(lon2 - lon1)
  val a = Math.sin(dLat/2) * Math.sin(dLat/2) +
          Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
          Math.sin(dLon/2) * Math.sin(dLon/2)
  val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
  earthRadius * c
})

步骤2:自连接筛选5公里内的城市对

用zipcode当唯一标识,自连接时避免重复配对:

val dfWithId = dfWithCoords.withColumn("city_id", col("zipcode"))

// 只连接ID更小的城市,避免(a,b)和(b,a)重复计算
val joinedDF = dfWithId.as("a")
  .join(dfWithId.as("b"), col("a.city_id") < col("b.city_id"))
  .withColumn("distance", haversineDistance(col("a.lat"), col("a.lon"), col("b.lat"), col("b.lon")))
  .filter(col("distance") <= 5.0)

步骤3:标记连通分量(把近的城市归为一组)

用Spark GraphX计算连通分量,把城市当节点,近的城市对当边:

import org.apache.spark.graphx._
import org.apache.spark.rdd.RDD

// 构建节点RDD:(city_id, (城市名, 人口))
val vertices: RDD[(Long, (String, Int))] = dfWithId
  .select(col("city_id").cast("long"), col("city"), col("pop"))
  .rdd.map(row => (row.getLong(0), (row.getString(1), row.getInt(2))))

// 构建边RDD:(起点ID, 终点ID, 距离)
val edges: RDD[Edge[Double]] = joinedDF
  .select(col("a.city_id").cast("long"), col("b.city_id").cast("long"), col("distance"))
  .rdd.map(row => Edge(row.getLong(0), row.getLong(1), row.getDouble(2)))

// 计算连通分量,得到每个城市所属的组ID
val graph = Graph(vertices, edges)
val connectedComponents = graph.connectedComponents().vertices

// 关联回原数据集
val groupedDF = dfWithId
  .join(spark.createDataFrame(connectedComponents).toDF("city_id", "group_id"), "city_id")

步骤4:按组汇总人口

最后一步简单聚合:

val resultDF = groupedDF
  .groupBy("group_id")
  .agg(
    collect_list("city").alias("同组城市"),
    sum("pop").alias("组内总人口")
  )

额外提示

如果数据量很大,自连接会生成超多中间数据,建议先按经纬度分网格,只在同一或相邻网格内计算距离,能大幅提升性能。

内容的提问来源于stack exchange,提问作者Guillermo García

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 11:52:21