【问题标题】:How can I produce a distance matrix for large datasets using Google Script?如何使用 Google Script 为大型数据集生成距离矩阵?
【发布时间】:2019-10-29 10:33:35
【问题描述】:

我目前正在制作一个脚本,该脚本将相互比较大约 90 个地址的列表。脚本的结果应该是一个列表,其中包含从彼此到达每个地址所花费的时间。

我在尝试解决此问题时遇到了一系列问题。主要问题是生成的距离矩阵将有 8100 个元素。 Google 脚本的最长执行时间是 30 分钟,因此脚本会一直超时。

有什么方法可以改进脚本以使其运行得更快?

此脚本的目的是生成一个包含 StartID、EndID 和 Time 的列表。然后,我将能够过滤列表以查找彼此相隔一小时内的地址。

谢谢!

function maps(origin, destination) {
  var driving = Maps.DirectionFinder.Mode.DRIVING
  var transit = Maps.DirectionFinder.Mode.TRANSIT
  var modeSet = driving
  var directions = Maps.newDirectionFinder()
  .setOrigin(origin)
  .setDestination(destination)
  .setMode(modeSet)
  .setOptimizeWaypoints(true)
  .getDirections()
  var result = directions
  return result;  
}


function GoogleMaps() {
 //get distance
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ABC");
  var outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("EFG");
  var lastrow = sheet.getLastRow();
  var lastcolumn = sheet.getLastColumn();
  var range = sheet.getRange(2, 3, lastrow-1, 3);
  //var range = sheet.getRange(2, 3, 3, 3);
     //Origin is in row 2, column 3
  var values = range.getValues();
  var output = []
  for (var i = 0; i < values.length; ++i)
  {
    var loop1 = values[i]
    var start = values[i][1]
    var startId = values[i][0]
    for (var j = 0; j < values.length; j++) {
      var loop2 = values[j]
      var end = values[j][1]
      var endId = values[j][0]
      var result = maps(start, end)
      var status = result.status
      try{
        var time = result.routes[0].legs[0].duration.value / 60;
        var row = [startId, endId, time]
        output.push(row)
      } catch(err){
        Logger.log(err);
      }
    }
   }    
  var outputLength = output.length
  var outputRange = outputSheet.getRange(1,1,outputLength,3);
  outputRange.setValues(output);
}

编辑:更新列表中的元素数量

【问题讨论】:

  • 90 * 90 就是 8100,这个怎么降到 3600?获取循环上方的列表是您创建output 数组的缓慢部分吗?较慢的部分是ouptutRange 的创建吗?是否在 try/catch 中产生任何错误,如果是这样,您是否可以在逻辑上检测到比 try/catch 更快的条件。
  • 是的,关于 8100 元素,您是对的。数据集从昨天开始更新,所以我还在想旧的数字。我使用 try/catch 块来尝试解决我在脚本的result.routes[0].legs[0].duration.value 方面遇到的另一个问题。 “腿”产生了一个未定义的错误。
  • 我会建议检查未定义的result.routes[0].legs === undefined,以及为什么它未定义?
  • 我用 3 行测试了 result.routes[0].legs[0].duration.value,得到了我想要的结果。这让我相信,也许问题在于腿不能针对如此大量的值起作用。我不确定如何解决这个问题。
  • 如何在 Python 中执行此操作(例如),这样您就不会有超时限制?

标签: javascript node.js google-maps google-apps-script google-sheets


【解决方案1】:

您要做的第一件事是减少在for 循环中执行的操作数。因此,让我们首先从算法的角度进行分析。

在您当前的实现中,您基本上是在一组 90 个值上计算 Cartesian Product,以生成一个包含 8100 个值的新集。

但是,该结果集中有许多冗余值,例如:

  1. 结果集包括使用相同地址作为起始和结束位置的计算。

  2. 2个地址之间的距离计算两次;这样地址 A 是起始地址,地址 B 是结束地址,在另一个迭代中,地址 A 是结束地址,地址 B 是起始地址。

    警告:我假设您在两个地址之间的运输过程中覆盖相同的距离,无论一个人的运输如何 方向(即 A 到 B 或 B 到 A)。您的情况可能并非如此 场景。

您可以通过使用称为组合的离散数学领域来消除这些冗余;更具体地说,使用这个可爱的公式:

如果我们让 n = 90r = 2 我们得到以下结果:

这意味着,在我们最优化的情况下,我们需要一种生成不超过 4005 个地址对的算法。

以此为目标,[掰手指] 是时候编写更优化的算法了!但出于说明目的和简洁的目的,让我们使用由一个字母组成的 4 个地址的较小样本量。以下数组就足够了:

var addresses = ['a', 'b', 'c', 'd'];

使用上述公式我们推导出有 6 个唯一的地址对,我们可以表示如下:

ab  bc  cd
ac  bd
ad

那么如何生成这些对呢?

如果您查看上面的表示,您会注意到一些事情:

  • 列数比数组中的地址数少一
  • 对于每个连续的列(从左到右),每列的地址对数减少 1; IE。有3对以'a'开头,2对以'b'开头,1对以'c'开头。
  • 另请注意,当您从一列前进到下一列时,连续列与前一列的起始字符没有任何配对; IE。第二列没有任何以“a”开头的对,第三列没有任何以“a”或“b”开头的对

让我们概括一下这些观察结果。给定 n 个地址数组,我们可以生成 n - 1 列。每列的长度缩小 1,使得第一列有 n - 1 对,第二列有 n - 2 对,第 3 列 n - 3 对等,其中每列由省略前列地址的对组合组成。

基于这些规则,我们可以如下设置for 循环(运行脚本,它将生成一个对象集合,其“开始”和“结束”属性代表唯一的地址对):

var addresses = ['a', 'b', 'c', 'd'];
var pairs = [];
var numColumns = addresses.length - 1;
var columnHeight;
var columnIndex;
var rowIndex;

for (columnIndex = 0; columnIndex < numColumns; columnIndex++) {

    columnHeight = numColumns - columnIndex;

    for (rowIndex = 0; rowIndex < columnHeight; rowIndex++) {
        pairs.push({
            "start":addresses[columnIndex],
            "end":addresses[columnIndex + rowIndex + 1]
        });
    }
 
}

console.log(pairs);

所以上面处理算法优化,您需要调整它以用于您的实现,但它应该作为一个很好的起点。然而,虽然生成 4005 个地址对相对较快,但通过 Map API 处理这些地址对以查找行进距离可能会很耗时。

如果您仍然设法用完 30 分钟的脚本执行配额,您可能需要考虑使用批处理技术,在这种技术中,您可以设置应用程序对较小批次的地址对进行计算,一次一个批次在给定的时期内。如果您正确设置应用程序,您甚至可以同时处理多个批次。但那是另一个帖子了。

【讨论】:

  • 谢谢。我尝试将您的建议应用于代码,但我一定做错了,因为脚本没有将电子表格中的地址分配给地址数组。
【解决方案2】:

这可能并不比您所拥有的性能更好,但尝试在此处将其分解为更模块化的解决方案,然后您可以决定优化哪个部分,也许可以通过一次在某个子集中进行;

function getValuesArray(values) {
  let valueArray = [];
  for (let i = 0; i < values.length; ++i) {
    valueArray.push({
      id: values[i][0],
      value: values[i][1]
    });
  }
  return valueArray;
}

function GoogleMaps() {
  //get distance
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ABC");
  var outputSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("EFG");
  var lastrow = sheet.getLastRow();
  var lastcolumn = sheet.getLastColumn();
  var range = sheet.getRange(2, 3, lastrow - 1, 3);
  //var range = sheet.getRange(2, 3, 3, 3);
  //Origin is in row 2, column 3
  var values = range.getValues();
  var output = [];
  let list1 = getValuesArray(values);
  // deep clone
  const clone = (items) => items.map(item => Array.isArray(item) ? clone(item) : { ...item
  });
  // might only need list1 but usin two for clarity here
  const list2 = clone(list1);
  const listWork = [];
  for (var a = 0; a < list1.length; a++) {
    for (var j = 0; j < list2.length; j++) {
      listWork.push({
          dest: list2[j].value,
          destId: list2[j].id,
          origin: list1[a].value,
          originId: list1[a].id
        }
      }
    }
  }
  let results = [];
  for (let w = 0; w < listWork.length; w++) {
    results.push(startId: listWork.originId, endId: listWork.destId, map: maps(listWork.origin, listWork.dest));
  }
  for (let r = 0; r < results.length; r++) {
    let result = results[r];
    // seems to not be used 
    //var status = result.map.status;
    let route = !!result.map.routes && result.map.routes[0] ? result.map.routes[0] : null;
    if (route !== null &&
      route.legs &&
      route.legs[0] &&
      route.legs[0].duration &&
      route.legs[0].duration.value) {
      let time = route.legs[0].duration.value / 60;
      let row = [result.startId, result.endId, time];
      output.push(row);
    }
  }

  let outputLength = output.length;
  let outputRange = outputSheet.getRange(1, 1, outputLength, 3);
  outputRange.setValues(output);
}

【讨论】:

  • 感谢您的建议。刷新我的日常执行后,我会尝试一下!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-12-01
  • 2023-03-10
  • 1970-01-01
  • 1970-01-01
  • 2017-08-02
  • 1970-01-01
  • 2020-05-16
相关资源
最近更新 更多