Geohash应用——附近乡镇信息挖掘(提升检索召回与准确)

摘要

         Geohash在LBS领域的应用开发很常见,常常应用于查询附近的人或门店等应用程序中。这里不再介绍Geohash的原理,其原理详见:GeoHash核心原理解析。 这里主要讲一个Geohash的另一种应用:挖掘热点地名/地址信息,补充实体POI(Point Of interest)信息,辅助扩大检索召回。

目录

一、背景介绍

二、解决方案

三、拓展与思考

四、代码实现

五、参考文献


一、背景介绍

在LBS检索中,用户检索query往往是where+what的检索,例如:q=桂平市西山镇长安小学。为了提高检索准确率,必然是会想办法解析出where,桂平市、西山镇,然后给用户查找what=长安小学。如果提供给检索的数据(这里指POI点)信息很全,比如:数据的地址字段(addr)含有“西山镇”,又或者名字字段(name)包含西山镇,例如:长安小学(西山镇分校),又或者有其他地址信息中包含“西山镇”,都可以辅助引擎检索召回,同时,当长安小学有很多时,数据字段越丰富,越准确,还能进一步提升排序的合理性。

 但现实世界往往是极其残酷的,说的不夸张一点,提供给检索引擎的数据,几乎都是东拼西凑的,数据制作工艺参差不齐。数据信息除了错,最大的问题之一就是信息不全,不够精细。特别是对于那些多源数据融合的检索数据,往往都存在类似问题。仅对LBS领域来说,一些小作坊的POI数据,往往存在缺失“省/市/区”三级以下的“地理信息”,即乡/镇/村/道路/道路门牌等。在这样的情况下,如何能够给那些“地理”信息不全的POI进行信息补全呢? 从而提高检索的召回率与准确率。 此文,正是要讨论解决的这一难题,它的解决方案,恰恰是Geohash的一个典型应用案例。 

二、解决方案

  思路:针对一个POI点,查找它附近点的地理信息。将附近点的地域信息经过一定的筛选和过滤,然后赋值给该POI点上的某个字段,从而补全该POI点的地理信息。

  不难发现,一提及“附近”,这就很容易想到Geohash。这里提出一个极其简单的解决方案,在实际应用中,各位还需结合自己的业务进行完善。

  前提条件

   <1> 已有“地名地址信息;行政区划”类目的POI数据,此类数据为:省、市、区、街道/乡/镇、村等行政区划的POI数据;

   <2> 每个POI数据均具有名称、地址、省、市、区、经纬度、行政区划编码、类别等基础字段。

   任务需求:利用以上“地名地址信息;行政区划”类目的POI数据,为其他POI点补充“地理信息” ,新增hot_place字段存放。

  【注】 仅补充区级以下,不用补充省、市、区一级的地理信息,因为它们已经是基础字段信息了。默认是必备的。

    具体解决方案与步骤

    <1> 利用Geohash算法,对已有“地名地址信息;行政区划”类目的POI数据进行编码,构建词表,存放在town_geohash.map文件中。

    <2> 遍历目标POI数据,利用经纬度字段计算出自身的Geohash值,再由该值查找出其附近8个格子的Geohash值。(类似9宫格,自身与其周边的8个方格,具体如图);

    <3> 利用步骤2得到的9个格子的Geohash值,查找构建的行政区划词表(town_geohash.map),找出每个格子对应的行政区划数据(名称和经纬度),并计算每个行政区划数据与目标POI点的距离。

     <4> 设定一个距离阈值r。目标POI点与要添加的行政区划数据的距离必须小于r,才能成为候选集。在候选集中,取距离最近的行政区划信息,补充至目标POI上,并存入hot_place字段。这里的筛选条件,是最简单的距离限制条件,并取最小值。

    以上4个步骤,就完成了任务需求。需要注意的是,步骤4中设定的距离阈值r,其实是影响到Geohash精度的选取的,即Geohash值的长度。这里,需要注意;因为步骤<1>构建词表与步骤<2>计算每个目标POI点的Geohash,需保持相同的Geohash精度值(即长度)。

三、拓展与思考

上一节,在实现步骤<4>提到,筛选条件仅用了距离因子进行了限制。并最终选取距离最小的一个。筛选条件,是一个值得思考和深究的地方。它极大影响了添加“地理信息”的准确率。

此文的任务需求是:增添行政区划数据,一个POI在一个级别也就只有一个行政区划信息。所以,找距离最小的一个,在这个任务场景下,仅靠距离因素限制,一般问题也不大,往往准确率也能达标。 但如果增加的是“热点地名,商圈”等地理信息。比如,王府井,理想国大厦,望京,中关村等。仅利用距离因子作为限制条件,现实情况下,准确率常常是不达标的。那么,应该怎么做呢?

如果是热点地名与商圈的地理信息补充,可以考虑,利用目标POI点与其周边格子所处位置,进行限制。比如,必须呈包含目标POI点的态势时,才能添加。怎么定义包含态势呢?这个读者可以自行定义与实现。这里举2个实例:

 a、目标点处于中心位,其余8个格子都包含“望京”这样一个商圈地理信息。这是典型的包含态势,目标POI点可增加商圈“望京”;

 b、目标点上/下(南北),左/右(东西),对角线格子均具有相同的地理信息,这种也可视为呈包含态势( 大致如图2-1所呈现的样子)。

 图2-1,展示了目标POI点以及周边8个格子的示意图。可以想象每个Geohash的格子都包含了一些地理信息。

                                Geohash应用——附近乡镇信息挖掘(提升检索召回与准确)_第1张图片                          
                                         图2-1,目标POI点与其周边8个格子的9宫格示意图

为了提高添加的“地理信息”的准确度。总结一下,本人能想到的限制条件主要有以下3方面:

1、距离条件是基础,必须有距离限制;

2、目标POI点所处格子与欲添加地理信息所处格子的位置态势进行限制;

3、为目标POI点增加某一个地理信息,该地理信息在单个格子出现的次数,以及它被距离条件筛选后,总体出现的次数。 

【注】某个地理信息出现的次数:可理解为有多少个POI具有该地理信息。

四、代码实现

代码实现,使用的是scala语言。Scala可以方便的调用Java语言的jar包。因此,你也可以理解为是Java实现的。这里有利用了Java的两个重要的jar包。

利用Spatial4j包计算两个经纬度之间的球面距离;利用ch.hsr.geohash包获取一个geohash周边8个网格(geohash)的方法


    org.locationtech.spatial4j
    spatial4j
    0.7



      ch.hsr
      geohash
      1.3

     以上两个包都能计算Geohash值。Geohash的长度对应了不同的精度。长度与精度对照表如下(最长为12):       

geohash码长度 宽度

高度

1 5,009.4km

4,992.6km

2 1,252.3km

624.1km

3 156.5km

156km

4 39.1km

19.5km

5 4.9km

4.9km

6 1.2km

609.4m

7 152.9m

152.4m

8 38.2m

19m

9 4.8m

4.8m

10 1.2m

59.5cm

11 14.9cm

14.9cm

12 3.7cm

1.9cm

       按照第二章解决方案的1~4的步骤实现。这里先要敲定距离阈值r,假定r=2公里,则Geohash的长度应选5(即4.9km,4.9km的格子)。由对照表可知如果选择Geohash长度为6(对应1.2km,0.6km),构造出的9宫格,是不满足需求的,会有漏掉满足距离目标POI点为2公里的行政区划POI点的。这是为什么,请大家自己思考吧。

       先把行政区划数据和结果词表geohash_map词表文件的样例贴出:

//这里对行政区划POI做了信息抽取,直接是town-name city  经纬度,存放到town.txt文件中,具体格式如下:
舒庄乡  周口市  114.454095,33.509907
幸福乡  乐山市  103.89755,28.939625
张家塬镇        宝鸡市  107.117532,34.699135
大林乡  忻州市  112.723693,38.856616
穆店乡  淮安市  118.605614,32.917239

//由town.txt构建的Geohash词表,存放在geohash_map词表中,第一列是5位的Geohash值,后面是城镇信息,具体格式如下:
wscey: 万福镇|吉安市|114.885236,27.419279
ws4wq: 新亨镇|揭阳市|116.289072,23.624153
wqry3: 和川镇|临汾市|112.23623,36.264385
wt45m: 石鼻镇|南昌市|115.573624,28.726617
ybe87: 铁林街道|伊春市|128.833531,47.864312
wq3d9: 免古池乡|临夏回族自治州|103.42043,35.619691    

步骤1: 利用行政区划POI构建geohash_map词表

 /**
   * @define 利用原始词表town.txt构建geohash_map词表.
   * @param fpath
   * @param output
   * @param len
   */
  def init_town_map(fpath: String, output: String, len: Int = 5): Unit = {
    val geohash_map = scala.collection.mutable.Map[String, List[String]]()
    Source.fromFile(fpath,"UTF-8").getLines().toList.filter(_.trim != "").foreach(line => 
    {
      val split_line = line.split("\t", -1)
      if (split_line.size == 3) {
        val town = split_line(0)
        val city = split_line(1)
        val loc = split_line(2).split(",", -1)
        val geohash_code = get_geohash_code(split_line(2))
        val tmplist = List[String](town + "|" + city + "|" + loc.mkString(","))
        if (geohash_map.contains(geohash_code)) {
          geohash_map(geohash_code) = geohash_map(geohash_code) ++ tmplist
        } else {
          geohash_map += (geohash_code -> tmplist)
        }
      }
    })

    val out = new PrintWriter(output)
    for((k,v) <- geohash_map){
      out.println(k+": " + v.mkString("\t"))
    }
    out.close()
  }

  /**
    * @define 依据经纬度以及指定长度,计算Geohash值.默认长度指定为5.
    * @param loc_str
    * @param len
    * @return
    */
  def get_geohash_code(loc_str:String,len:Int = 5):String = {
    val loc = loc_str.split(",",-1)
    val lon = loc(0).toDouble
    val lat = loc(1).toDouble
    val geohash_code = GeohashUtils.encodeLatLon(lat, lon, len)
    geohash_code
  }

步骤2:这里给出了如何找出9宫格的Geohash值。代码实现时,不仅找到了9个方格的geohash,还给每个方案设定了标记值,标注方向。标记值与格子位置的对应关系如下图所示。有了格子相对目标POI点的方向标注,后续才可能实现第三节所说的依据“位置态势”进行限制。其中,目标POI所处的格子,方向标注是MY。

                   Geohash应用——附近乡镇信息挖掘(提升检索召回与准确)_第2张图片

import ch.hsr.geohash.GeoHash
import org.locationtech.spatial4j.context.SpatialContext
import org.locationtech.spatial4j.distance.DistanceUtils
import org.locationtech.spatial4j.io.GeohashUtils
 /**
   * @define 包括自己一共会找到9个格子(涵盖自己和相邻的8个格子),分别用标
   *  记"MY,N,NE,E,SE,S,SW,W,NW"标记出格子的方位,其中MS,是该点自己所处格子的标记.
   * @param lon
   * @param lat
   * @return
   */
  def find_nearby_geohash(lon:Double,lat:Double):Array[Tuple2[GeoHash,String]] = {
    val nearby_town_array = ArrayBuffer[Tuple2[GeoHash,String]]()
    try{
      val geohash:GeoHash = GeoHash.withCharacterPrecision(lat,lon,5)
      nearby_town_array += Tuple2(geohash,"MY")
      val nearby_town = geohash.getAdjacent
      //N, NE, E, SE, S, SW, W, NW
      val direct_flag_list = "N,NE,E,SE,S,SW,W,NW".split(",",-1)
      for(i <- 0 until nearby_town.size){
        val geohash_item = nearby_town(i)
        val direct_flag = direct_flag_list(i)
        nearby_town_array += Tuple2(geohash_item,direct_flag)
      }
    }catch {
      case e:Exception => {}
    }
    nearby_town_array.toArray
  }

步骤3:利用步骤2得到的9个格子的Geohash值,查找构建的行政区划词表(town_geohash.map),找出每个格子对应的行政区划数据(名称和经纬度),并计算每个行政区划数据与目标POI点的距离。

//存放所有找到的行政区划数据(行政区划的一些信息值,存放为String类型,与目标POI的距离,Double类型) 
val all_nearby_towns = ListBuffer[Tuple2[String,Double]]()

//9个格子的geohash值和方向标注均被保存在一个存放为Tuple2类型的数组中。遍历这个数组,获取每个格子中的行政区划数据(名称,城市,经纬度)。 
val nearby_town:Array[Tuple2[GeoHash,String]] = find_nearby_geohash(lon,lat)  //find_nearby_geohash 在上面步骤2实现了该方法
if(nearby_town.size > 0){
      nearby_town.foreach(geohash_item => {
        val geohash_code = geohash_item._1.toBase32
        val direct_flag = geohash_item._2
        if(geohash_map.contains(geohash_code)){
          val nearby_town_list =  geohash_map(geohash_code).map(town_item => {
            var item = Tuple2[String,Double](town_item,10000.0)
            val tmparr = town_item.split("\\|",-1)
            if(tmparr.size == 3){
              val town = tmparr(0)
              val gcity = tmparr(1)
              val loc2 = tmparr(2).split(",",-1)
              if(city == "" ){
                val distance = get_distance(Tuple2(lon,lat),Tuple2(loc2(0).toDouble,loc2(1).toDouble))
                item = (town_item+"|"+nearby_geohash,distance)
              }else{
                if(city.startsWith(gcity) || gcity.startsWith(city)){
                  val distance = get_distance(Tuple2(lon,lat),Tuple2(loc2(0).toDouble,loc2(1).toDouble))
                  item = (town_item+"|"+nearby_geohash,distance)
                }
              }
            }
            item
          }).filter(_._2 <= 2.0 )           //此处,直接将距离大于2公里的行政信息都已剔除了
          if(nearby_town_list.size > 0){
            all_nearby_towns ++=  nearby_town_list
          }
        }
      })
}

 /**
   * @define 提供一对经纬度坐标,计算两个点的球面距离
   * @param loc1
   * @param loc2
   * @return
   */
  def get_distance(loc1:Tuple2[Double,Double], loc2:Tuple2[Double,Double]):Double = {
    val geo:SpatialContext = SpatialContext.GEO
    val geo_shape = geo.getShapeFactory
    val p1 = geo_shape.pointXY(loc1._1,loc1._2)
    val p2 = geo_shape.pointXY(loc2._1,loc2._2)
    val distance:Double = geo.calcDistance(p1,p2) * DistanceUtils.DEG_TO_KM
    get_litpoint_level(distance,2)  //单位:km,该函数仅是设定获取小数点后几位。
  }
  
  def get_litpoint_level(num:Double,level:Int):Double = {
    val  bg:BigDecimal = new BigDecimal(num)
    bg.setScale(level, BigDecimal.ROUND_HALF_UP).doubleValue()
  }    

步骤4:取距离最小的作为添加的行政区划信息。这里的代码实现方式是:将所有存放行政区化信息的List,按照距离排序(升序)。然后,取第一个元素,即距离最小的那个行政区划信息。

//all_nearby_towns按照距离进行升序排序,步骤3的代码中已经限定了存放的元素都必须小于2公里。
//所以,这里没有重复限定2公里。都必定是<=2公里的元素
val sort_nearby_towns = all_nearby_towns.sortWith(_._2 < _._2)  
val nearest_town = sort_nearby_towns.head    //取第1个元素,作为添加的行政区划信息。

       另外,这里认为,不存在同时有1个以上的行政区划的点,与目标POI点的距离一样。默认,最小值仅存在一个。因此,代码实现没有考虑上述极端情况。   

      写到这里,4个步骤均以实现完成了。拓展一节中提到的更多筛选限制的条件。其实,在步骤3或步骤4中均可增加代码实现。比如,上述步骤3中的代码实现,如果认真阅读,可以发现,代码实现中,多了一个城市的限定比较。行政区划的数据信息,它所归属的城市必须与目标POI所属城市相同,才能进入候选集。否则,无论远近,均不能作为候选集。

五、参考文献

1、Java中“附近的人”实现方案讨论及代码实现

2、按距离搜索邻近城市的一种实现

3、Geohash求当前区域周围8个区域编码的一种思路

你可能感兴趣的:(数据挖掘,算法,scala,java,经验分享)