Расчет статистики по зонам

Работа со статистикой по зонам в геопространственном анализе опирается на идею пространственной агрегации: набор точек, линий или объектов объединяется в заранее определённые области (зоны), после чего внутри каждой зоны вычисляются численные характеристики. В Turf.js этот подход реализуется через комбинацию предикатов принадлежности, функций перебора геометрий и статистических операций над наборами данных.

Зоны обычно представлены полигонами, которые могут соответствовать административным границам, сетке тайлов, пользовательским областям интереса или результатам кластеризации. Точки — это наблюдения: события, координаты объектов, измерения. Главная задача заключается в сопоставлении каждой точки с зоной и последующем вычислении агрегированных метрик.

Базовая модель зональной статистики

Формально процесс состоит из трёх этапов:

  1. Определение зон Зоны задаются как FeatureCollection<Polygon | MultiPolygon>.

  2. Привязка объектов к зонам Каждая точка проверяется на принадлежность полигону.

  3. Агрегация значений Внутри каждой зоны вычисляются статистики: сумма, среднее, минимум, максимум, количество.

Ключевое отличие Turf.js от серверных ГИС-систем заключается в том, что все операции выполняются в памяти браузера или Node.js, без пространственного индекса по умолчанию. Это влияет на производительность и требует аккуратного выбора алгоритмов при больших данных.

Проверка принадлежности точки зоне

Основой зональной статистики является операция point-in-polygon. В Turf.js она реализуется через:

  • booleanPointInPolygon

Алгоритм использует лучевой метод (ray casting), определяя, пересекает ли горизонтальный луч границы полигона нечётное число раз.

Пример базовой проверки:

import booleanPointInPolygon from "@turf/boolean-point-in-polygon";

const inside = booleanPointInPolygon(point, polygon);

Эта операция становится ядром всех последующих агрегаций, поскольку определяет, в какую зону попадает каждая наблюдаемая точка.

Структура данных для зональной агрегации

Типичная структура входных данных:

const zones = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      properties: { id: 1, name: "Zone A" },
      geometry: {
        type: "Polygon",
        coordinates: [...]
      }
    }
  ]
};

const points = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      properties: { value: 10 },
      geometry: {
        type: "Point",
        coordinates: [30.5, 50.2]
      }
    }
  ]
};

Каждая зона может содержать собственные свойства, в которые будут записаны результаты статистики.

Простое агрегирование: количество точек

Самая базовая метрика — количество точек в зоне. Она вычисляется перебором всех точек и проверкой их принадлежности:

import booleanPointInPolygon from "@turf/boolean-point-in-polygon";

function countPointsInZones(zones, points) {
  return {
    ...zones,
    features: zones.features.map(zone => {
      let count = 0;

      for (const pt of points.features) {
        if (booleanPointInPolygon(pt, zone)) {
          count++;
        }
      }

      return {
        ...zone,
        properties: {
          ...zone.properties,
          count
        }
      };
    })
  };
}

Такой подход имеет сложность O(n × m), где n — количество зон, m — количество точек.

Агрегация числовых значений

Часто точки содержат измеряемые параметры: температуру, количество пользователей, интенсивность события. Тогда требуется вычисление суммы, среднего и других метрик.

Сумма и среднее

function zoneSumAndMean(zones, points, property) {
  return {
    ...zones,
    features: zones.features.map(zone => {
      let sum = 0;
      let count = 0;

      for (const pt of points.features) {
        if (booleanPointInPolygon(pt, zone)) {
          sum += pt.properties[property];
          count++;
        }
      }

      return {
        ...zone,
        properties: {
          ...zone.properties,
          sum,
          mean: count > 0 ? sum / count : 0
        }
      };
    })
  };
}

Такой подход позволяет строить тепловые карты на уровне зон, а не отдельных точек.

Использование collect для группировки значений

В Turf.js существует функция collect, предназначенная для извлечения значений из точек внутри полигонов.

import collect from "@turf/collect";

Она позволяет агрегировать свойства точек по зонам без ручного перебора.

Принцип работы:

  • вход: полигоны, точки, поле значений
  • выход: полигоны с массивом значений

После этого статистика вычисляется уже по массиву.

const collected = collect(zones, points, "value", "values");

Далее можно вычислить метрики:

collected.features.forEach(zone => {
  const values = zone.properties.values || [];

  const sum = values.reduce((a, b) => a + b, 0);
  const mean = values.length ? sum / values.length : 0;

  zone.properties.sum = sum;
  zone.properties.mean = mean;
});

Оптимизация через предварительную фильтрацию

При большом количестве точек проверка каждого объекта на каждый полигон становится узким местом. Для оптимизации используется предварительное ограничение через bounding box.

Turf.js предоставляет:

  • bbox
  • booleanDisjoint
  • pointsWithinPolygon

Использование pointsWithinPolygon

import pointsWithinPolygon from "@turf/points-within-polygon";

const pts = pointsWithinPolygon(points, polygon);

Эта функция возвращает только те точки, которые гарантированно находятся внутри полигона, снижая количество проверок.

Зональная статистика по регулярной сетке

Частый сценарий — разбиение территории на сетку (grid) и расчет статистики по каждой ячейке.

import squareGrid from "@turf/square-grid";
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";

Процесс:

  1. Создаётся сетка
  2. Каждая ячейка считается зоной
  3. Точки агрегируются внутри ячеек
const grid = squareGrid(bbox, cellSize);

grid.features.forEach(cell => {
  let count = 0;

  for (const pt of points.features) {
    if (booleanPointInPolygon(pt, cell)) {
      count++;
    }
  }

  cell.properties.count = count;
});

Такой подход используется для heatmap-аналитики, оценки плотности и кластерного анализа.

Взвешенные статистики

Если каждая точка имеет вес (например, количество пользователей или интенсивность события), применяется взвешенная агрегация.

function weightedSum(zones, points, valueField, weightField) {
  return {
    ...zones,
    features: zones.features.map(zone => {
      let sum = 0;
      let weightSum = 0;

      for (const pt of points.features) {
        if (booleanPointInPolygon(pt, zone)) {
          const value = pt.properties[valueField];
          const weight = pt.properties[weightField];

          sum += value * weight;
          weightSum += weight;
        }
      }

      return {
        ...zone,
        properties: {
          ...zone.properties,
          weightedMean: weightSum ? sum / weightSum : 0
        }
      };
    })
  };
}

Взвешенные метрики особенно важны в демографии, логистике и анализе трафика.

Работа с несколькими слоями данных

Зональная статистика часто выполняется не по одной, а по нескольким категориям данных одновременно.

Пример: в каждой зоне нужно посчитать:

  • количество точек
  • сумму значений
  • количество уникальных категорий
function multiStats(zones, points) {
  return zones.features.map(zone => {
    let count = 0;
    let sum = 0;
    const categories = new Set();

    for (const pt of points.features) {
      if (booleanPointInPolygon(pt, zone)) {
        count++;
        sum += pt.properties.value;
        categories.add(pt.properties.type);
      }
    }

    return {
      ...zone,
      properties: {
        ...zone.properties,
        count,
        sum,
        uniqueCategories: categories.size
      }
    };
  });
}

Такой подход используется в аналитических панелях, где зоны выступают как единицы визуализации.

Использование пространственных индексов вручную

Turf.js не включает встроенный R-tree, но его можно комбинировать с внешними структурами данных. Однако даже без них можно снизить нагрузку через предварительный bbox-фильтр:

import bboxPolygon from "@turf/bbox-polygon";
import booleanIntersects from "@turf/boolean-intersects";

Сначала проверяется пересечение bounding box:

if (!booleanIntersects(point, zoneBboxPolygon)) {
  continue;
}

Только затем выполняется точная проверка принадлежности.

Сложные сценарии: иерархические зоны

Иногда зоны вложены: страна → регион → город → район. В этом случае статистика рассчитывается по нескольким уровням.

Подход:

  1. Сначала агрегация на нижнем уровне
  2. Затем свёртка вверх по иерархии
function aggregateHierarchy(level1, level2) {
  level2.features.forEach(parent => {
    parent.properties.count = 0;

    for (const child of level1.features) {
      if (booleanPointInPolygon(child, parent)) {
        parent.properties.count += child.properties.count || 1;
      }
    }
  });
}

Такая модель позволяет строить каскадные отчёты.

Производительность и ограничения

Ключевые факторы производительности:

  • количество точек
  • количество зон
  • сложность геометрии
  • частота вызова booleanPointInPolygon

Оптимизации:

  • предварительный bbox-фильтр
  • разбиение данных на чанки
  • кэширование результатов принадлежности
  • уменьшение точности координат при необходимости

При масштабах десятки тысяч точек и сотни зон чистый перебор становится неэффективным, и требуется внешняя индексация или серверная обработка.

Типовые ошибки при расчёте зональной статистики

  • использование полигонов с некорректной топологией
  • отсутствие проверки MultiPolygon
  • смешение координатных систем
  • повторный пересчёт одних и тех же проверок
  • игнорирование граничных случаев (точка на границе полигона)

Особенно критичен последний случай, поскольку Turf.js может трактовать границу как принадлежность в зависимости от реализации алгоритма.

Практическая модель агрегации для приложений

В реальных приложениях обычно строится единый pipeline:

  1. Загрузка зон
  2. Загрузка точек
  3. Предфильтрация по bbox
  4. Распределение по зонам
  5. Вычисление статистики
  6. Обогащение свойств зон
  7. Передача в визуализацию (GeoJSON слоя)

Такой конвейер обеспечивает предсказуемость вычислений и совместимость с картографическими библиотеками (Mapbox, Leaflet, OpenLayers).