Объяснение бинирования данных

Предыстория и отказ от ответственности. В компании, в которой я работаю, мы разделили наших клиентов на четыре класса в зависимости от их платежного поведения. Как правило, при сегментировании клиентов в бизнес-среде используются три параметра: давность (как недавно клиент совершил покупку), частота (как часто клиент совершает покупки) и денежный (сколько клиент тратит на каждую покупку). Однако в этом случае я буду использовать частоту только для объяснения концепции биннинга. В будущем я предоставлю более полное объяснение сегментации клиентов.

Но что такое биннинг?

Биннинг данных — это процесс организации ваших данных в виде конечного диапазона интервалов. Этот диапазон интервалов называется «бинами».

Объединение данных, также известное как группирование данных, представляет собой метод предварительной обработки данных, используемый в машинном обучении и интеллектуальном анализе данных для группировки непрерывных значений данных в дискретные интервалы или «бины». Биннинг может служить нескольким целям, в том числе уменьшить количество ошибок данных за счет сглаживания колебаний данных, получить представление о распределении данных и преобразовать действительные признаки в дискретные интервалы, также известное как однократное кодирование.

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

const width = (max - min) / number_of_bins
const bins_values = [min, min + width, min + 2 * width, ..., min + number_of_bins * width]

Можете ли вы показать пример?

Чтобы проиллюстрировать концепцию объединения данных, давайте рассмотрим практический пример с использованием Typescript. Предположим, у нас есть набор данных, который записывает количество покупок, сделанных клиентами за определенный период времени. Набор данных представлен в следующем массиве:

const trxFreq = [229, 293, 394, 39, 39, 49, 93, 100, 29, 48, 188]

В этом примере значения представляют количество покупок, сделанных каждым покупателем. Мы можем использовать бинирование данных, чтобы сгруппировать эти значения в дискретные интервалы, что упрощает анализ данных и получение информации о покупательском поведении наших клиентов.

Чтобы проанализировать набор данных о частоте покупок клиентов, необходимо разделить данные на четыре отдельные ячейки, помеченные от «D-клиенты» до «A-клиенты». Для этого мы сначала создадим простую структуру данных для хранения наших бинов.

/**
 * Structure of each bin
 */
export interface Bin {
  /**
   * Bucket.
   */
  item: {
    value: number
    label?: string
  }

  /**
   * Values in a bin
   */
  values: number[]
}

/**
 * Array type of bins
 */
export type Bins = Bin[]

Чтобы собрать данные о частоте покупок клиентов, мы создадим функцию с именем toBin. Эта функция примет массив чисел (наш набор данных), желаемое количество интервалов и необязательный массив меток. Функция toBin будет реализовывать наш алгоритм биннинга, группируя набор данных в указанное количество бинов и возвращая массив бинированных данных.

Код для функции toBin может выглядеть примерно так:

/**
 * Convert input dataset array of numbers into bins.
 *
 * @param data dataset array
 * @param binSize Number of bins
 * @param labels Optional labels for the bins
 */
export function toBins(data: number[], binSize: number, labels?: string[]) {
   // Implementation
}

Если для функции toBin не указан необязательный массив меток, алгоритм будет использовать значения бинов для маркировки бинов. Например, если мы делим данные на четыре ячейки, ячейки могут быть помечены следующим образом:

  • Группа 1: покупки с наименьшей частотой / удачливые клиенты.
  • Группа 2: Покупки с низкой частотой / малозаинтересованные клиенты
  • Группа 3: Покупки с умеренной частотой / Нормальные клиенты.
  • Группа 4: частые покупки/хорошие клиенты.

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

const sampleCount = data.length

// Sort data in descending order
const sortedData = data.sort((lhs, rhs) => lhs - rhs)
// Find min and max samples
const minSample = sortedData[0]
const maxSample = sortedData[sampleCount - 1]

// Create equal spaced bins
const binWidth = (maxSample - minSample) / binSize
const bins: Bins = Array(binSize)
  .fill({ values: [] })
  .map((bin, i) => ({
    item: {
      value: minSample + binWidth * i,
      label: labels?.[i]
    },
    ...bin
   }))

Чтобы начать процесс бинирования в функции toBin, мы создаем подсчет количества элементов в нашем наборе входных данных, взяв длину массива. Затем мы сортируем набор данных в порядке возрастания и записываем минимальное и максимальное значения, присутствующие в наборе данных, для использования при расчете ширины бина.

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

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

// Search for the next chunk from the sorted array to add into the bin values
let minIndex = 0
let maxIndex = sampleCount - 1

for (let i = 0; i < binSize; i++) {
  while (
    minIndex < sampleCount &&
    sortedData[minIndex] < bins[i].item.value
  ) {
    minIndex++
  }
  while (
    maxIndex >= 0 &&
    sortedData[maxIndex] > bins[i].item.value + binWidth
  ) {
    maxIndex--
  }
  for (let j = minIndex; j <= maxIndex; j++) {
    bins[i].values.push(sortedData[j])
  }

  // Slice item from the sorted array into the bin
  bins[i].values = sortedData.slice(minIndex, maxIndex + 1)

  // Get the maxIndex ready for the next iteration.
  maxIndex = sampleCount - 1
}

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

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

Чтобы увидеть алгоритм в действии, ниже приведен полностью функционирующий пример.

export interface Bin {
  
  item: {
    value: number
    label?: string
  }

  values: number[]
}

export type Bins = Bin[]

export function toBins(data: number[], binSize: number, labels?: string[]) {
  const sampleCount = data.length

  if (binSize >= sampleCount) {
    throw new RangeError(
      `Bin size ${binSize} should be less than sample size: ${sampleCount}`
    )
  }

  // Sort data in descending order
  const sortedData = data.sort((lhs, rhs) => lhs - rhs)
  // Find min and max samples
  const minSample = sortedData[0]
  const maxSample = sortedData[sampleCount - 1]

  // Create equal spaced bins
  const binWidth = (maxSample - minSample) / binSize
  const bins: Bins = Array(binSize)
    .fill({ values: [] })
    .map((bin, i) => ({
      item: {
        value: minSample + binWidth * i,
        label: labels?.[i]
      },
      ...bin
    }))

  let minIndex = 0
  let maxIndex = sampleCount - 1

  for (let i = 0; i < binSize; i++) {
    while (
      minIndex < sampleCount &&
      sortedData[minIndex] < bins[i].item.value
    ) {
      minIndex++
    }
    while (
      maxIndex >= 0 &&
      sortedData[maxIndex] > bins[i].item.value + binWidth
    ) {
      maxIndex--
    }

    // Slice a bucket
    bins[i].values = sortedData.slice(minIndex, maxIndex + 1)

    // Get the maxIndex ready for the next iteration.
    maxIndex = sampleCount - 1
  }

  return bins
}

Код в действии…

После того, как функция toBin реализована, ее можно вызывать из клиентского кода для создания массива объектов Bin, представляющих сгруппированные данные. Для этого клиентский код должен передать входной набор данных, желаемое количество интервалов и, необязательно, массив меток в функцию toBin.

В качестве примера мы можем предоставить образцы транзакций и вызвать функцию toBin для этого набора данных, чтобы создать массив из Bin объектов, представляющих сгруппированные данные.

const trxFreq = [229, 293, 394, 39, 39, 49, 93, 100, 29, 48, 188]
const labels = ['D-Customers', 'C-Customers', 'B-Customers', 'A-Customers']
const bins = toBins(trxFreq, labels.length, labels)

console.log(bins)
// Prints
// [
//    {
//      "item": { "value": 29, "label": "D-Customers" },
//      "item": [29, 39, 39, 48, 49, 93, 100 ]
//    },
//    {
//      "iem": { "value": 120.25, "label": "C-Customers" },
//      "values": [ 188 ]
//    },
//    {
//      "item": { "value": 211.5, "label": "B-Customers" },
//      "values": [ 229, 293 ]
//    },
//    {
//      "item": { "value": 302.75, "label": "A-Customers" },
//      "values": [ 394 ]
//    }
// ]

Результирующий массив объектов Bin, созданный функцией toBin, можно использовать для вычисления гистограммы путем построения длины массива значений по соответствующей метке для каждого интервала.

Стоит отметить, что реализованный нами алгоритм имеет временную сложность O(n log n) из-за необходимости сортировки входного набора данных. Кроме того, пространственная сложность алгоритма составляет O(k), где k — количество ячеек.

Заключительные заметки и выводы:

Подводя итог, в этом уроке мы рассмотрели простой алгоритм бинирования равной ширины и предоставили пример того, как его можно использовать для группировки клиентов на основе их частоты покупок. Биннинг — это распространенный метод предварительной обработки данных, который можно использовать для уменьшения ошибок, получения информации о наборе данных или преобразования реальных значений в дискретные интервалы. Однако с этим методом связаны некоторые проблемы, такие как определение соответствующего размера ячейки для данного набора данных.

В зависимости от конкретного варианта использования могут использоваться альтернативные методы для организации набора данных в определенные группы, такие как алгоритм k-ближайших соседей (kNN). Важно тщательно рассмотреть конкретные требования проекта, чтобы определить наиболее подходящий метод предварительной обработки для использования.

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

Наконец, Вот репозиторий github для этого урока. Спасибо!