Рубрики
Без рубрики

Генерализованное сгребание для взвешивания обследования

В мире опросов очень распространено, что наши приобретенные ответы должны быть взвешены по порядку … Tagged with Ruby, Python, статистика.

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

Например, мы могли бы осмотреть 100 респондентов и 150 респондентов, но нацеливались на соотношение мужчин/женщин 48%/52%. В этом простом случае мы могли бы достичь целевого соотношения, взвешивая мужские ответы с фактором 0,48/(100/(100 +.2 и взвешивание женских ответов по 0,52/(150/(100 +.867 . Технический термин для этого метода вычисления веса – Пост-стратификация Анкет

Однако в более сложном сценарии, где у нас есть много Различные измеримые демографические цели, как мы можем определить веса для всех респондентов?

Сгребая

В Potloc очень часто, что наши клиенты желают обследования населения, соответствующего многим таким целям. Например, у нас могут быть цели, которые выглядят так:

  • 42% мужчины
  • 58% женщины
  • 20% студентов
  • 80% не студентов
  • 15% владельцев собак

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

Итеративная пропорциональная подгонка

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

Большой! Проблема решена!

… но что, если бы мы могли сделать еще лучше? 🤔

Генерализованное сгребание

Помимо удовлетворения демографических целей, наиболее желательным свойством для весов является то, что они должны быть как можно ближе к 1 Анкет Действительно, веса, которые действительно являются большими, означают, что ответы этих респондентов будут учитываться намного больше, чем «средний» респондент в результатах нашего опроса. Например, респондент с весом 10 будет учитываться в 10 раз больше, чем средний респондент, и в 100 раз больше, чем респондент с весом 0,1. Точно так же небольшие веса означают, что некоторые ответы окажут очень мало влияния на конечные результаты.

К сожалению, итеративная пропорциональная подгонка ничего не делает, чтобы поощрять веса быть близкими к 1 , что приводит к неоптимальным весам. Вот где Генерализованный съемки Алгоритм, введенный Deville et al. (1992), вступает в игру.

Примечание: Здесь мы попадаем в более математическую часть этого сообщения в блоге 🤓. Не заботитесь об этой части? Не волнуйтесь! Просто перейдите к следующему разделу!

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

где G ( x ) G (x) G ( x ) это ездить Функция, которая поощряет веса быть близкими к 1 , W W W вектор веса, T T T является вектором целей (в абсолютных числах, а не в процентах) и X X X это ( numrespondents × numtargets ) (\ text {numrespondents} \ times \ text {numtargets}) ( numrespondents × numtargets ) Матрица ответов. Матрица X X X двоичный, где клетки заполнены «1», если респондент принадлежит к целевой категории и в противном случае.

Другими словами, это говорит о том, что мы хотим оптимизировать веса, чтобы быть как можно ближе к 1, одновременно удовлетворяя целевые ограничения. Это достигается путем минимизации функции G ( x ) G (x) G ( x ) что выглядит так (обратите внимание на глобальный минимум в x = 1 x = 1 x = 1 !):

Генерализованный алгоритм сгребания

Хотя можно решить эту проблему оптимизации, используя общие методы, такие как Последовательное программирование наименьших квадратов , авторы генерализованного сбоя разработали более эффективный и надежный алгоритм для этой конкретной проблемы:

  1. Инициализировать переменные
    • A ( numrespondents × 1 ) (\ text {numrespondents} \ times 1) ( numrespondents × 1 ) вектор W W W к одному
    • A ( numtargets × 1 ) (\ text {numtargets} \ times 1) ( numtargets × 1 ) вектор λ \ lambda λ на нули
    • A ( numrespondents × numrespondents ) (\ text {номер респонденты} \ times \ text {num respondents}) ( numrespondents × numrespondents ) квадратная матрица H H H к матрице личности
  2. Пока веса не сходились
    1. λ = λ + ( X T H X ) 1 ( T X T w ) \ lambda = \ lambda + (x^t h x)^{ – 1} (t – x^t w) λ = λ + ( X T H X ) 1 ( T X T w )
    2. w = G 1 ( X λ ) w^{-1} (x \ lambda) w = G 1 ( X λ )
    3. H = диаг ( G 1 ( X λ ) ) H = \ text {diag} ({g^{-1}} ‘(x \ lambda)) H = диаг ( G 1 ( X λ ))

Здесь, G 1 ( x ) G^{-1} (x) G 1 ( x ) является обратным производным функции сгребания, т.е. E x e^x E x Анкет G 1 {G^{-1}} ‘ G 1 Это производное, в данном случае также E x e^x E x Анкет

Реализация

Несмотря на то, что в R есть много реализаций этого алгоритма, мы не смогли найти один в Ruby, который мог бы хорошо играть с нашей кодовой базой и быть легко подлежащим обслуживанию. Поэтому мы решили сделать свое собственное и поделиться им здесь для тех, кто ищет что -то подобное. Мы начали с внедрения в Python с популярным Numpy библиотека:

import numpy as np

def raking_inverse(x):
  return np.exp(x)

def d_raking_inverse(x):
  return np.exp(x)

def graking(X, T, max_steps=500, tolerance=1e-6):
  # Based on algo in (Deville et al., 1992) explained in detail on page 37 in
  # https://orca.cf.ac.uk/109727/1/2018daviesgpphd.pdf

  # Initialize variables - Step 1
  n, m = X.shape
  L = np.zeros(m) # Lagrange multipliers (lambda)
  w = np.ones(n) # Our weights (will get progressively updated)
  H = np.eye(n)
  success = False

  for step in range(max_steps):
    L += np.dot(np.linalg.pinv(np.dot(np.dot(X.T, H), X)), (T - np.dot(X.T, w))) # Step 2.1
    w = raking_inverse(np.dot(X, L)) # Step 2.2
    H = np.diag(d_raking_inverse(np.dot(X, L))) # Step 2.3

    # Termination condition:
    loss = np.max(np.abs(np.dot(X.T, w) - T) / T)
    if loss < tolerance:
        success = True
        break

  if not success: raise Exception("Did not converge")
  return w

Реализация Ruby

После проверки алгоритма в Python мы затем приступили к воспроизведению его в Ruby. Для этого мы должны были найти эквивалент Numpy который мы нашли в Нумео . Numo – потрясающая библиотека для векторных и матричных операций, а его Линальг Суб-библиотека была идеальной для нас, так как нам нужно было вычислить матричную псевдо-неверную. Это позволило нам перевести код на Ruby почти по строке:

require "numo/narray"
require "numo/linalg"

def raking_inverse(x)
  Numo::NMath.exp(x)
end

def d_raking_inverse(x)
  Numo::NMath.exp(x)
end

def graking(X, T, max_steps: 500, tolerance: 1e-6)
  # Based on algo in (Deville et al., 1992) explained in detail on page 37 in
  # https://orca.cf.ac.uk/109727/1/2018daviesgpphd.pdf

  # Initialize variables - Step 1
  n, m = X.shape
  L = Numo::DFloat.zeros(m)
  w = Numo::DFloat.ones(n)
  H_diag = Numo::DFloat.ones(n)

  success = false

  max_steps.times do
    L += Numo::Linalg.pinv((X.transpose * H_diag).dot(X)).dot(T - X.transpose.dot(w)) # Step 2.1
    w = raking_inverse(X.dot(L)) # Step 2.2
    H_diag = d_raking_inverse(X.dot(L)) # Step 2.3

    # Termination condition:
    loss = ((T - X.transpose.dot(w)).abs / T).max
    if loss < tolerance
      success = true
      break
    end
  end

  raise StandardError, "Did not converged" unless success
  w
end

Возможно, вы заметили, что код не совсем точно соответствует алгоритму, описанному выше, в частности, шаги 2.1 и 2.3. Это потому, что мы обнаружили, что это было значительно быстрее с Numo для хранения редкой матрицы H H H как плоский вектор h_matrix_diagonal так как он содержит только значения на диагонали. В результате, шаг принятия продукта Икс T H X^t h X T H может быть переписан как X.transpose * h_matrix_diagonal , используя неявное вещание Numo.

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

С этими несколькими строками кода мы теперь можем поддерживать сложные сценарии взвешивания, имея весь наш код в нашем прекрасном Ruby Monolith 🎉

Заинтересованы в том, что мы делаем в Potloc? Присоединяйся к нам! Мы нанимаем 🚀

Рекомендации

Оригинал: “https://dev.to/potloc/generalized-raking-for-survey-weighting-2d1d”