В мире опросов очень распространено, что наши приобретенные ответы должны быть взвешены, чтобы получить выборку, которая является представитель некоторой целевой популяции. Этот процесс Веселье Просто состоит из назначения вес (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), вступает в игру.
Примечание: Здесь мы попадаем в более математическую часть этого сообщения в блоге 🤓. Не заботитесь об этой части? Не волнуйтесь! Просто перейдите к следующему разделу!
Авторы этой статьи сформулировали проблему взвешивания в качестве метода ограниченного оптимизации, в котором цель состоит в том, что веса как можно ближе к одному, и где ограничение состоит в том, что цели соответствуют. Математически это выглядит так:
где
Другими словами, это говорит о том, что мы хотим оптимизировать веса, чтобы быть как можно ближе к 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 к матрице личности
- A
- Пока веса не сходились
λ = λ + ( 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 )w = G − 1 ( X λ ) w^{-1} (x \ lambda) w = G − 1 ( X λ )H = диаг ( G − 1 ′ ( X λ ) ) H = \ text {diag} ({g^{-1}} ‘(x \ lambda)) H = диаг ( G − 1 ′ ( 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_matrix_diagonal
так как он содержит только значения на диагонали. В результате, шаг принятия продукта X.transpose * h_matrix_diagonal
, используя неявное вещание Numo.
На практике мы оптимизируем этот код чуть дальше, выходя на раннем этапе, когда это возможно, когда это возможно (например, если наша потеря становится nan
) и позволяя передавать в качестве ввода начальное значение для вектора лямбдас
Если мы считаем, что есть значение инициализации лучше, чем дефолт.
С этими несколькими строками кода мы теперь можем поддерживать сложные сценарии взвешивания, имея весь наш код в нашем прекрасном Ruby Monolith 🎉
Заинтересованы в том, что мы делаем в Potloc? Присоединяйся к нам! Мы нанимаем 🚀
Рекомендации
- Отличная докторская диссертация Гарета Дэвиса по этому вопросу
- Deville et al. 1992 Обобщенные процедуры сгребания бумага
Оригинал: “https://dev.to/potloc/generalized-raking-for-survey-weighting-2d1d”