В настоящее время я создаю сценарий, который будет сравнивать список из примерно 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);
}

РЕДАКТИРОВАТЬ: обновленное количество элементов в списке

1
Madison Miller 29 Окт 2019 в 13:33
90 * 90 это 8100, как это уменьшить до 3600? Является ли получение списка над циклом медленной частью создания массива output? Является ли медленная часть созданием ouptutRange ?, возникли ли какие-либо ошибки в try / catch, И если да, то можете ли вы вместо этого логически определить это условие, которое будет быстрее, чем try / catch.
 – 
Mark Schultheiss
29 Окт 2019 в 14:11
Да, насчет 8100 элементов вы правы. Набор данных был обновлен со вчерашнего дня, поэтому я все еще думал о старых числах. Я использовал блок try / catch, чтобы попытаться решить другую проблему, с которой я столкнулся с аспектом result.routes[0].legs[0].duration.value скрипта. «Ноги» выдавали неопределенную ошибку.
 – 
Madison Miller
29 Окт 2019 в 14:15
Затем я бы предложил проверить, возможно, этот неопределенный result.routes[0].legs === undefined, а также почему он НЕ определен?
 – 
Mark Schultheiss
29 Окт 2019 в 14:19
Я протестировал result.routes[0].legs[0].duration.value на трех строках и получил желаемые результаты. Это заставляет меня думать, что, возможно, проблема в том, что ноги не могут работать против такого большого количества ценностей. Я не знаю, как решить эту проблему.
 – 
Madison Miller
29 Окт 2019 в 14:27
Как насчет того, чтобы сделать это в Python (например), чтобы у вас не было ограничения на тайм-аут?
 – 
Jescanellas
29 Окт 2019 в 14:30

2 ответа

Первое, что вам нужно сделать, это уменьшить количество операций, выполняемых в циклах for. Итак, давайте начнем с анализа этого, но с алгоритмической точки зрения.

В вашей текущей реализации вы в основном вычисляете декартово произведение для набора из 90 значений, чтобы произвести новый набор, состоящий из 8100 значений.

Однако в этом наборе результатов есть несколько избыточных значений, например:

  1. Набор результатов включает вычисления, в которых один и тот же адрес используется как в качестве начального, так и конечного местоположения.

  2. Расстояние между двумя адресами рассчитывается дважды; таким образом, что адрес A является начальным адресом, а адрес B является конечным адресом, а в другой итерации адрес A является конечным адресом, а адрес B является начальным адресом.

    ПРЕДОСТЕРЕЖЕНИЕ: я предполагаю, что вы преодолеваете одинаковое расстояние во время транзита между двумя адресами независимо от транзита одного из них. направление (например, от A к B или от B к A). Это может быть не так в вашем сценарий.

Вы можете устранить эту избыточность, используя область дискретной математики, называемую комбинаторикой; более конкретно, используя эту прекрасную формулу:

formula

Если мы допустим n = 90 и r = 2 , мы получим следующее:

formula

Это означает, что в наиболее оптимальном варианте нам нужен алгоритм, который генерирует не более 4005 пар адресов.

С этой нашей целью [щелкает пальцами] пора написать более оптимальный алгоритм! Но в иллюстративных целях и в интересах краткости позволяет использовать меньший размер выборки из 4 адресов, составленных из одной буквы. Следующего массива должно хватить:

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

Используя вышеупомянутую формулу, мы делаем вывод, что существует 6 уникальных пар адресов, которые мы можем представить следующим образом:

ab  bc  cd
ac  bd
ad

Так как же создать эти пары?

Если вы посмотрите на изображение выше, вы заметите несколько вещей:

  • Количество столбцов на единицу меньше количества адресов в массиве
  • С каждым последующим столбцом (слева направо) количество пар адресов в столбце уменьшается на 1; т.е. есть 3 пары, которые начинаются с буквы «a», 2 пары начинаются с буквы «b», 1 пара начинается с буквы «c».
  • Также обратите внимание, что при переходе от одного столбца к следующему в последующих столбцах нет пар с начальным символом предыдущих столбцов; т.е. во 2-м столбце нет пар, начинающихся с 'a', а в 3-м столбце нет пар, начинающихся с 'a' или 'b'

Обобщим эти наблюдения. Учитывая массив адресов n , мы можем сгенерировать n - 1 столбцов. Длина каждого столбца уменьшается на 1, так что первый столбец содержит n - 1 пар, а второй столбец - n - 2 . пары, третий столбец - n - 3 пары и т. д., где каждый столбец состоит из пар комбинаций, в которых пропущены адреса из предыдущих столбцов.

Основываясь на этих правилах, мы можем настроить цикл for следующим образом (запустите скрипт, и он сгенерирует коллекцию объектов, свойства "start" и "end" которых представляют уникальные пары адресов):

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 происходит относительно быстро, обработка этих пар адресов для определения пройденного расстояния через API карты, вероятно, займет много времени.

Если вам все же удастся исчерпать 30-минутную квоту на выполнение скрипта, вы можете рассмотреть возможность использования методов пакетной обработки, когда вы настраиваете свое приложение для выполнения вычислений на меньших пакетах пар адресов, по одному пакету за раз в течение заданного времени. период. Возможно, вы даже сможете обрабатывать несколько пакетов одновременно, если правильно настроите приложение. Но это пост в другой раз.

1
TheAddonDepot 30 Окт 2019 в 06:29
Спасибо за это. Я пытался применить ваше предложение к коду, но я, должно быть, делаю что-то не так, поскольку сценарий не назначает адреса из электронной таблицы массиву адресов.
 – 
Madison Miller
31 Окт 2019 в 12:40

Возможно, это не лучше того, что у вас есть для производительности, но попробуйте здесь разбить его на более модульное решение, тогда вы можете решить, какую часть оптимизировать, возможно, выполняя это в некотором подмножестве за раз;

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);
}
0
Mark Schultheiss 29 Окт 2019 в 15:45
Спасибо за предложение. Я попробую, как только обновится мое ежедневное исполнение!
 – 
Madison Miller
29 Окт 2019 в 17:29