背景
该需求在笔者本科的毕业设计过程中产生,需要生成一定量的数据用于聚类分析测试数据集。
笔者经过大量的搜索,依然没有找到能符合项目需要的数据集,最终决定通过代码的方式生成若干订单数据。为了方便其他人查看,笔者将查询到的数据集统一放在这里。
滴滴盖亚数据 | https://outreach.didichuxing.com/ | 需要申请 | |
UberMovement | https://link.zhihu.com/?target=https%3A//movement.uber.com/ | 国际网约车巨头,笔者需要国内的数据,pass | |
阿里天池数据集 | https://tianchi.aliyun.com/dataset?spm=5176.14154004.J_3941670930.21.31fe5699J4PqSP | 可以搜索到滴滴公布的数据,搜索关键字”滴滴“ | |
知乎作者总结 | https://www.zhihu.com/question/24335537 | 比较全面 |
尽管存在诸多真实的数据集,但都不满足笔者做聚类分析的基本需求,其中最有可能满足的是2020_ccf滴滴时空预测_训练数据集,但是数据没有任何描述信息,直接从数据入手无法理解。
基本目标数据
字段 | 类型 |
订单开始时间 | DATE |
订单结束时间 | DATE |
起点(经纬度) | VARCHAR |
终点(经纬度) | VARCHAR |
成果截图
![]() | ![]() |
![]() | ![]() |
![]() |
代码实现
地点选择
为了尽可能保证数据真实性,作为打车地点应该选择为该城市热门地点。以哈尔滨为例,通过美团进行搜索,结果即可认为是该城市热门地点。
- 抓包找到数据请求路径
curl "https://apimobile.meituan.com/group/v4/poi/pcsearch/105?uuid=6961927388a74eba9ec9.1652337967.1.0.0&userid=1699913514&limit=32&offset=32&cateId=-1&q=^%^E5^%^93^%^88^%^E5^%^B0^%^94^%^E6^%^BB^%^A8&token=Im7ycrPXlIs7yI8TXXXXXXXXXXXX
-H "Accept: */*" ^
-H "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6" ^
-H "Connection: keep-alive" ^
-H "Cookie: uuid=6961927388a74eXXXXXXXXXXXXXXXXXXXXX
-H "Origin: https://hrb.meituan.com" ^
-H "Referer: https://hrb.meituan.com/" ^
-H "Sec-Fetch-Dest: empty" ^
-H "Sec-Fetch-Mode: cors" ^
-H "Sec-Fetch-Site: same-site" ^
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36" ^
-H "sec-ch-ua: ^\^" Not A;Brand^\^";v=^\^"99^\^", ^\^"Chromium^\^";v=^\^"101^\^", ^\^"Microsoft Edge^\^";v=^\^"101^\^"" ^
-H "sec-ch-ua-mobile: ?0" ^
-H "sec-ch-ua-platform: ^\^"Windows^\^"" ^
--compressed
发现请求通过设置offset 参数为分页查询limit 偏移量,所以可以通过设置该值(0,32,64,96,···)完成对全部数据的查询。如上图抓包数据所示
- 通过postman 将浏览器抓的包导入并转换成java代码,如上图postman转java代码所示
// 执行抓包代码
package com.example.point.predict;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.example.point.domain.mapper.HotAreaDao;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Service
public class ExecutorTask {
// hotAreaDao 基于HotArea 的持久层接口,使用MP框架,继承BaseMapper接口
@Autowired
private HotAreaDao hotAreaDao;
public void getHotArea() {
int index = 0;
while (index++ <= 36) {
List<HotArea> list = doGetHotArea(String.valueOf(32 * index));
assert list != null;
// 持久化到数据库中
list.forEach(a -> hotAreaDao.insert(a));
}
}
private List<HotArea> doGetHotArea(String offset) {
HttpUrl url = HttpUrl.parse("https://apimobile.meituan.com/group/v4/poi/pcsearch/105?uuid=6961927388a74eba9ec9.1652337967.1.0.0&userid=1699913514&limit=32&cateId=-1&q=%E5%93%88%E5%B0%94%E6%BB%A8&token=Im7ycrPXlIs7yI8TnP3eLq2MH8UAAAAAuxEAALrOIZCcFyKVfLB6YYHG4mtoojKPoeQebjDm6R80cgwPID2K83VeJZ9-A9VoUDwc5A&sort=default")
.newBuilder()
.addQueryParameter("offset", offset)
.build();
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
Request request = new Request.Builder()
.url(url)
.method("GET", null)
.addHeader("Accept", "*/*")
.addHeader("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
.addHeader("Connection", "keep-alive")
.addHeader("Cookie", "uuid=6961927388a74eba9ec9.1652337967.1.0.0; _lx_utm=utm_source%3Dbing%26utm_medium%3Dorganic; _lxsdk_cuid=180b70485d4c8-06c7b6d70175c7-12333272-e1000-180b70485d4c8; ci=105; mtcdn=K; userTicket=SULzbnlKOnLeJcncXywbuPQZSqyBbqEFvtOYsQMx; _yoda_verify_resp=1ovN6a48LK40Lon%2BFF2pB9OP9nhfGcUiJEjzL%2FSrhFyDao4sU7K4toyhPpT3WnmWNj4%2Fm3M87qwvZTfv7fQw96fksSiW6OR94P6rXB7RfTPoRCGBxUrRev%2FrWv0OEWQDzuRKsp%2BWhmKlKKSF2gbVhiU%2FxcSaAtH26KdlwUPotsQB4lkYvduG3ofYO%2BhRkTMEJaSoGgqwO6uf%2B0v2NB6vVQZkWD5JJK0pSx2CEZcb0%2FnEWywKHKD7gCvRw2FbrBvZPEVxsCfq8H71Gzd7NwMr7NI%2BvcjfU345F9tN%2FAJHl8GAOQ%2FfrJgjcloer1ET3X%2FeVepex3txcxkzjPbHt4tixqcn%2F8jXHHhXMrkluYWfW6E7%2BoBNyoHMScijYJmDr3EH; _yoda_verify_rid=15289e4b3a40b014; u=1699913514; n=ccr763202343; lt=Im7ycrPXlIs7yI8TnP3eLq2MH8UAAAAAuxEAALrOIZCcFyKVfLB6YYHG4mtoojKPoeQebjDm6R80cgwPID2K83VeJZ9-A9VoUDwc5A; mt_c_token=Im7ycrPXlIs7yI8TnP3eLq2MH8UAAAAAuxEAALrOIZCcFyKVfLB6YYHG4mtoojKPoeQebjDm6R80cgwPID2K83VeJZ9-A9VoUDwc5A; token=Im7ycrPXlIs7yI8TnP3eLq2MH8UAAAAAuxEAALrOIZCcFyKVfLB6YYHG4mtoojKPoeQebjDm6R80cgwPID2K83VeJZ9-A9VoUDwc5A; token2=Im7ycrPXlIs7yI8TnP3eLq2MH8UAAAAAuxEAALrOIZCcFyKVfLB6YYHG4mtoojKPoeQebjDm6R80cgwPID2K83VeJZ9-A9VoUDwc5A; unc=ccr763202343; firstTime=1652338204933; _lxsdk_s=180b70485d5-ced-43f-b8c%7C%7C129")
.addHeader("Origin", "https://hrb.meituan.com")
.addHeader("Referer", "https://hrb.meituan.com/")
.addHeader("Sec-Fetch-Dest", "empty")
.addHeader("Sec-Fetch-Mode", "cors")
.addHeader("Sec-Fetch-Site", "same-site")
.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36")
.addHeader("sec-ch-ua", "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"101\", \"Microsoft Edge\";v=\"101\"")
.addHeader("sec-ch-ua-mobile", "?0")
.addHeader("sec-ch-ua-platform", "\"Windows\"")
.build();
try {
Response response = client.newCall(request).execute();
String json = response.body().string();
JSONObject jsonObject = JSONObject.parseObject(json).getJSONObject("data");
List<HotArea> list = new ArrayList<>();
JSONArray array = jsonObject.getJSONArray("searchResult");
for (int i = 0; i < array.size(); i++) {
JSONObject item = array.getJSONObject(i);
HotArea hotArea = new HotArea.HotAreaBuilder()
.city("哈尔滨")
.sourceId(73856242)
.title(item.getString("title"))
.latitude(item.getString("latitude"))
.longitude(item.getString("longitude"))
.showtype(item.getString("showType"))
.backcatename(item.getString("backCateName"))
.areaname(item.getString("areaname"))
.originJson(item.toJSONString())
.img(item.getString("imageUrl"))
.build();
list.add(hotArea);
}
return list;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
// HotArea 实体类,也就是具体将美团上获取的 所需要的数据封装成类
package com.example.point.predict;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* hot_area
* @author
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class HotArea implements Serializable {
/**
* 主键id
*/
@TableId
private Integer id;
/**
* 城市
*/
private String city;
/**
* 源id
*/
private Integer sourceId;
private String title;
/**
* 经度
*/
private String latitude;
/**
* 维度
*/
private String longitude;
/**
* 地点类型(学校、景点···)
*/
private String showtype;
private String backcatename;
/**
* ___路(学府路,和平路···)
*/
private String areaname;
/**
* 原始json
*/
private String originJson;
private String img;
private static final long serialVersionUID = 1L;
}
// HotArea 的数据持久化接口,xml不再粘贴
package com.example.point.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.point.predict.HotArea;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface HotAreaDao extends BaseMapper<HotArea> {
int deleteByPrimaryKey(Integer id);
int insert(HotArea record);
int insertSelective(HotArea record);
HotArea selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(HotArea record);
int updateByPrimaryKey(HotArea record);
}
通过创建一个Test 方法,执行ExecutorTask的getHotArea 方法即可得到热点地区信息。如上图热点地区数据所示.
- 生成测试订单数据集
订单数据表
字段名 | 类型 | 描述信息 |
or_id | bigint | 主键id |
orgin | varchar | 起点 |
destination | varchar | 终点 |
begin_time | datetime | 开始时间 |
endtime | datetime | 结束时间 |
node | varchar | 备注信息 |
cost | double | 费用 |
client_id | varchar | 乘客id |
driver_id | varchar | 司机id |
status | varchar | 订单状态 |
from | varchar | 起点经纬度(以逗号分割) |
to | varchar | 终点经纬度(以都好分割) |
distance | double | 距离 |
duration | bigint | 花费时间 |
在HotArea表中,随机获取两个不同的地址,作为一条订单记录的起点和终点,并随机一个2022-5-1 到当前的一个时间作为订单开始时间。通过腾讯地图开放api 批量距离计算(矩阵)来计算。调用该接口可以返回行车距离和行车时间。这样即可完成表中关键数据的生成。
package com.example.point.predict;
import com.alibaba.fastjson.JSONObject;
import com.example.point.app.MapService;
import com.example.point.domain.entity.PoOrder;
import com.example.point.domain.mapper.HotAreaDao;
import com.example.point.domain.mapper.PoOrderDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.text.ParseException;
import java.util.Date;
import java.util.Random;
@Service
public class GeneralOrderData {
@Autowired
private HotAreaDao hotAreaDao;
@Autowired
private PoOrderDao orderMapper;
public void generalData() throws IOException, ParseException {
// hotArea 表中存在992 条数据,随机0-992 之间的数并以主键id查询
int fromIndex = (int) (Math.random() * 993);
int toIndex = (int) (Math.random() * 993);
// 避免重复
while (fromIndex == toIndex) {
toIndex = new Random(992).nextInt();
}
HotArea fromHotArea = hotAreaDao.selectByPrimaryKey(fromIndex);
HotArea toHotArea = hotAreaDao.selectByPrimaryKey(toIndex);
// 获取行车距离,行车时间;MapService 内部通过OkHttp3完成对Tencent 地图api的访问
String json = new MapService().getDistance("driving", fromHotArea.getLatitude().concat(",").concat(fromHotArea.getLongitude()), toHotArea.getLatitude().concat(",").concat(toHotArea.getLongitude()));
// 通过fastjson进行解析,接下来的distance和duration即为距离和时间,单位米和秒
JSONObject jsonObject = JSONObject.parseObject(json)
.getJSONObject("result")
.getJSONArray("rows")
.getJSONObject(0)
.getJSONArray("elements")
.getJSONObject(0);
String distance = jsonObject.getString("distance");
String duration = jsonObject.getString("duration");
// 随机2022-5-1 之后的一个时间戳,作为订单开始时间。并与duration相加计算可得到结束时间
long day = 864000;
long startTime = (long) (Math.random() * (day + 1)) + 1651334400;
long endTime = startTime + Long.parseLong(duration);
// 订单信息封装类
PoOrder poOrder = PoOrder.builder()
.beginTime(new Date(startTime * 1000))
.endtime(new Date(endTime * 1000))
.cost(new MapService().getCost(Double.parseDouble(distance)))
.orgin(fromHotArea.getTitle())
.destination(toHotArea.getTitle())
.from(fromHotArea.getLatitude().concat(",").concat(fromHotArea.getLongitude()))
.to(toHotArea.getLatitude().concat(",").concat(toHotArea.getLongitude()))
.driverId(String.valueOf(1000 + (int)(Math.random() * 101)))
.clientId(String.valueOf(2000 + (int)(Math.random() * 101)))
.status("完成")
.distance(Double.valueOf(distance))
.duration(Long.valueOf(duration))
.build();
// 持久化订单记录
orderMapper.insert(poOrder);
}
}
// 订单封装类
package com.example.point.domain.entity;
import java.io.Serializable;
import java.util.Date;
import lombok.Builder;
import lombok.Data;
/**
* po_order
* @author
*/
@Data
@Builder
public class PoOrder implements Serializable {
private Long orId;
/**
* 起点
*/
private String orgin;
/**
* 终点
*/
private String destination;
/**
* 开始时间
*/
private Date beginTime;
/**
* 结束时间
*/
private Date endtime;
/**
* 备注信息
*/
private String node;
/**
* 费用
*/
private Double cost;
/**
* 乘客id
*/
private String clientId;
/**
* 司机id
*/
private String driverId;
/**
* 订单状态
*/
private String status;
/**
* 起点经纬度(以逗号分割)
*/
private String from;
/**
* 终点经纬度(以都好分割)
*/
private String to;
/**
* 行车距离
*/
private Double distance;
/**
* 行车时间)
*/
private Long duration;
private static final long serialVersionUID = 1L;
}
// 订单持久化接口,BaseMapper 是Mybatis Plus 提供的mapper接口,其实并不需要理解这个类,实际上就是将一条订单保存到数据库中,可以有很多种方法
package com.example.point.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.point.domain.entity.PoOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface PoOrderDao extends BaseMapper<PoOrder> {
int deleteByPrimaryKey(Long orId);
int insert(PoOrder record);
int insertSelective(PoOrder record);
PoOrder selectByPrimaryKey(Long orId);
int updateByPrimaryKeySelective(PoOrder record);
int updateByPrimaryKey(PoOrder record);
}
// 批量执行 生成订单数据
package com.example.point.predict;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.text.ParseException;
@SpringBootTest
class GeneralOrderDataTest {
@Autowired
private GeneralOrderData generalOrderData;
@Test
void generalData() {
try {
for (int i = 1; i < 999; i++) {
System.out.println(i);
if (i % 50 == 0) {
System.out.println("休眠 " + i);
// 休眠是因为地图api访问频度太高会拒绝
Thread.sleep(10000);
}
generalOrderData.generalData();
}
} catch (IOException e) {
e.printStackTrace();
} catch (ParseException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果如上图 生成的订单数据 所示
总结
通过该项目基本完成了所需要的订单数据生成,但是思考发现数据距离真实数据还有很大差别。
- 订单起点终点:选择仍然只是理论上的热点,实际上可能存在大量在小区、医院、公司的订单数据。
- 订单开始时间:代码中只做了随机,实际上在早晚高峰也是打车的高峰期,单纯随机并不能反映出这一定。
- 订单真实性:发现某些数据的里程数在40公里及以上,订单价格以每公里1.9 元计算最高达到了900元,实际上这种订单大概率不会发生。
- 优化手段:可以通过出租车gps 数据进行分析,获取到打车热力图并以此作为订单起点。对时间进行加权随机,对公里数做出限制,如果发现公里数大于30则进行筛选,仅生成1/4 的大公里数据。
数据及源码
热点城市数据(HotArea)992条 | hot_area.sql |
订单数据(PoOrder)约1500条 | po_order.sql |
代码 | 稍后总结 |