Как рассчитать или приблизить медианное значение списка без сохранения списка

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

В идеале я хотел бы написать свой код примерно так:

var medianCalculator = new MedianCalculator();
foreach (var value in SourceData)
{
  medianCalculator.Add(value);
}
Console.WriteLine("The median is: {0}", medianCalculator.Median);

Все, что мне нужно, это актуальный код MedianCalculator!

Обновление. Некоторые люди спрашивают, известны ли свойства значений, для которых я пытаюсь вычислить медианное значение. Ответ положительный. Одно значение - от -25 до -0,5 с шагом 0,5. Другой также с шагом 0,5 от -120 до -60. Думаю, это означает, что я могу использовать гистограмму в той или иной форме для каждого значения.

Спасибо

Ник


person Nick Randell    schedule 12.03.2009    source источник
comment
Вы ищете алгоритм общего назначения (в этом случае это кажется невозможным) или у ваших входных данных есть некоторые свойства, которые можно было бы использовать для разработки конкретного решения?   -  person mouviciel    schedule 12.03.2009
comment
Реализация C ++ путем stackoverflow.com/users/25188/john-d-cook вычисления процентилей в памяти. связанные приложения, codeproject.com/KB/recipes/TailKeeper.aspx См. также stackoverflow.com/questions/1058813/   -  person Alessandro Jacopson    schedule 07.06.2012
comment
Вы можете сохранить текущую пробу (например, с помощью отбора проб из резервуара) любого размера, который вам удобен. Затем, в конце концов, вы можете вычислить медианное значение выборки. (Это также можно расширить с помощью бегущей медианы, если вы хотите иметь быстрый доступ к ней во время чтения.)   -  person Thomas Ahle    schedule 14.09.2018


Ответы (11)


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

person David Z    schedule 12.03.2009
comment
Мне нравится этот ответ. Это отличная идея, поскольку я знаю типы сохраняемых значений и могу достаточно легко построить гистограмму. - person Nick Randell; 12.03.2009

Есть статистика «лекарств». Он работает, сначала настраивая k массивов, каждый длиной b. Значения данных подаются в первый массив, и, когда он заполнен, вычисляется медиана и сохраняется в первой позиции следующего массива, после чего первый массив используется повторно. Когда второй массив заполнен, медиана его значений сохраняется в первой позиции третьего массива и т. Д. И т. Д. Вы уловили идею :)

Это просто и довольно надежно. Ссылка здесь ...

http://web.ipac.caltech.edu/staff/fmasci/home/astro_refs/Remedian.pdf

Надеюсь это поможет

Майкл

person michael    schedule 08.04.2009
comment
есть также более новая статья: sciencedirect.com/science/article/pii/S0304397513004519 - person S.A.; 07.04.2018

Я использую эти инкрементные / рекурсивные средние и медианные оценки, которые используют постоянное хранилище:

mean += eta * (sample - mean)
median += eta * sgn(sample - median)

где eta - небольшой параметр скорости обучения (например, 0,001), а sgn () - знаковая функция, которая возвращает одно из значений {-1, 0, 1}. (Используйте константу eta, если данные нестационарны и вы хотите отслеживать изменения во времени; в противном случае для стационарных источников вы можете использовать что-то вроде eta = 1 / n для среднего оценщика, где n - количество наблюдаемых на данный момент выборок ... к сожалению, это не работает для медианного оценщика.)

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

Кроме того, я модифицировал инкрементную медианную оценку для оценки произвольных квантилей. Как правило, функция квантиля сообщает вам значение, которое делит данные на две части: p и 1-стр. Следующее оценивает это значение постепенно:

quantile += eta * (sgn(sample - quantile) + 2.0 * p - 1.0)

Значение p должно быть в пределах [0,1]. Это существенно сдвигает симметричный вывод функции sgn () {-1,0,1} в сторону одной стороны, разделяя выборки данных на две ячейки разного размера (доли p и 1-p от данные меньше / больше квантильной оценки соответственно). Обратите внимание, что для p = 0,5 это сводится к средней оценке.

Я бы хотел увидеть оценщик в инкрементальном режиме подобной формы ...

(Примечание: я также разместил это в аналогичной теме здесь: Онлайн-алгоритмы (итераторы) для оценки статистической медианы, режима, асимметрии, эксцесса?)

person Tyler Streeter    schedule 27.01.2010
comment
Эта медианная оценка не работает для негауссовских распределений. Представьте себе распределение, в котором у вас есть только два числа: x видели n раз, а x + y видели n + m раз. Если m или y относительно велики, оценка прерывается. Пример: вы видите 100 10m + 1 раз, затем 1 100k раз. Истинная медиана = 100. Расчетная медиана = 1. - person Eli; 14.09.2013
comment
Конечно, это пока оценка. Обратите внимание, что проблему можно решить с помощью большого количества итераций n. Для n, стремящегося к бесконечности, ошибка сходится к eta, но даже это можно преодолеть, постепенно уменьшая eta. - person Tim Kuipers; 26.11.2013
comment
Это было именно то, что мне было нужно! Так просто! Спасибо! - person Ihle; 11.01.2019
comment
Что касается среднего, зачем использовать приближение вместо точной рекурсивной формулы mean += (sample - mean) / N (см. лучшее описание здесь)? - person bluenote10; 23.05.2020
comment
@ bluenote10 Хорошее замечание. Использование константы eta хорошо для отслеживания нестационарных источников, тогда как eta = 1 / n (эквивалентно тому, что вы написали) хорошо для стационарного случая. Я обновил сообщение, чтобы отразить это различие. - person Tyler Streeter; 23.05.2020

Вот сумасшедший подход, который вы можете попробовать. Это классическая проблема потоковых алгоритмов. Правила

  1. У вас ограниченная память, скажем O(log n), где n - количество элементов, которые вы хотите
  2. Вы можете взглянуть на каждый элемент один раз и тут же решить, что с ним делать, если вы его сохраните, это будет стоить памяти, если вы выбросите его, он исчезнет навсегда.

Идея поиска медианы проста. Произвести выборку O(1 / a^2 * log(1 / p)) * log(n) элементов из списка случайным образом, это можно сделать с помощью отбора проб из коллектора (см. предыдущий вопрос). Теперь просто верните медиану из выбранных элементов, используя классический метод.

Гарантия заключается в том, что индекс возвращенного элемента будет (1 +/- a) / 2 с вероятностью не менее 1-p. Таким образом, существует вероятность отказа p, вы можете выбрать ее, отобрав больше элементов. И он не вернет медиану или не гарантирует, что значение возвращаемого элемента где-то близко к медиане, просто при сортировке списка возвращенный элемент будет близок к половине списка.

Этот алгоритм использует O(log n) дополнительного пространства и работает в линейном времени.

person Pall Melsted    schedule 12.03.2009
comment
Привет, не могли бы вы объяснить, что такое a и p, что они означают? Кстати, ссылка, которую вы предоставили, в настоящее время отсутствует. - person user7364588; 02.03.2019
comment
Вы можете выбрать a и p достаточно маленькими, это будет означать, что вы получите лучшее приближение с большей вероятностью, но временная сложность увеличивается. - person Pall Melsted; 16.04.2019

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

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

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

Создайте структуру с N ячейками. Вы сохраните значение X для каждого перехода слота (всего N + 1 значений), а также заполнение корзины.

Поток в ваших данных. Запишите первые N + 1 значений. Если поток заканчивается до этого, отлично, у вас загружены все значения, и вы можете найти точную медиану и вернуть ее. В противном случае используйте значения для определения вашей первой гистограммы. Просто отсортируйте значения и используйте их как определения бункеров, каждый бункер имеет популяцию 1. Это нормально, когда есть дубли (бины с 0 шириной).

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

Но этого недостаточно ... вам все равно нужно АДАПТИРОВАТЬ гистограмму к данным, которые передаются в потоке. Когда корзина переполняется, вы теряете информацию о подраспределении этой корзины. Вы можете исправить это, адаптировавшись на основе некоторой эвристики ... Самый простой и надежный - это если контейнер достигает определенного порогового значения (что-то вроде 10 * v / N, где v = # значений, наблюдаемых до сих пор в потоке, и N - количество ящиков), вы РАЗДЕЛИТЕ эту переполненную корзину. Добавьте новое значение в середину бункера, дайте каждой стороне половину исходной популяции бункера. Но теперь у вас слишком много корзин, поэтому вам нужно УДАЛИТЬ корзину. Хорошей эвристикой для этого является поиск корзины с наименьшим произведением численности населения и ширины. Удалите его и объедините с его левым или правым соседом (в зависимости от того, какой из соседей имеет наименьшее произведение ширины и населения). Выполнено! Обратите внимание, что при объединении или разделении ячеек информация теряется, но это неизбежно ... у вас есть только фиксированное хранилище.

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

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

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

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

person SPWorley    schedule 12.03.2009
comment
Спасибо - это хороший ответ, но может оказаться слишком дорогим для моих требований. Мне нужно иметь много медиан для большой географической области, с разными медианами для каждой области размером 200 на 200 метров. - person Nick Randell; 13.03.2009
comment
Есть несколько хороших идей, но я застрял на одном - когда вы разделяете корзину пополам, как вы решаете, сколько из этой корзины попадет во вложенную корзину №1, а сколько - во вложенную корзину №2? Кажется, вам нужно будет записывать каждое значение в корзине (поскольку корзина может делиться много раз). - person j_random_hacker; 09.04.2009
comment
JRH: Вы разделяете пополам и распределяете половину населения в каждую ячейку. Мы не храним больше информации о внутреннем распределении данных суббина, чтобы делать что-то еще, и разделение в основном предназначено для лучшего разрешения данных с этого момента. - person SPWorley; 10.04.2009
comment
@Arno: Спасибо за разъяснения. Мне кажется, что подход с потерями, но работоспособный, +1. - person j_random_hacker; 10.04.2009

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

  • в среднем, если вы знаете, что данные распределены симметрично
  • или вычислить правильную медианную величину небольшого подмножества данных (которое умещается в памяти) - если вы знаете, что ваши данные имеют одинаковое распределение по выборке (например, что первый элемент имеет то же распределение, что и последний)
person Grzenio    schedule 12.03.2009

Предложение Дэвида кажется наиболее разумным подходом к приближению медианы.

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

M n = M n-1 + ((V n - M n-1) / n)

Где M n - среднее из n значений, M n-1 - предыдущее среднее, а V n - новое значение.

Другими словами, новое среднее значение - это существующее среднее плюс разница между новым значением и средним значением, деленная на количество значений.

В коде это будет выглядеть примерно так:

new_mean = prev_mean + ((value - prev_mean) / count)

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

person GrahamS    schedule 08.04.2009
comment
Они просят медианы - person GilbertS; 23.02.2020

Найдите Min и Max списка, содержащего N элементов, с помощью линейного поиска и назовите их HighValue и LowValue Let MedianIndex = (N + 1) / 2

Двоичный поиск 1-го порядка:

Повторите следующие 4 шага, пока LowValue ‹HighValue.

  1. Получите MedianValue приблизительно = (HighValue + LowValue) / 2

  2. Получить NumberOfItemsWhichAreLessThanorEqualToMedianValue = K

  3. равно K = MedianIndex, тогда верните MedianValue

  4. такое K> MedianIndex? затем HighValue = MedianValue Else LowValue = MedianValue

Это будет быстрее без использования памяти

Двоичный поиск 2-го порядка:

LowIndex = 1 HighIndex = N

Повторяйте следующие 5 шагов, пока (LowIndex ‹HighIndex)

  1. Получить приблизительное значение DistrbutionPerUnit = (HighValue-LowValue) / (HighIndex-LowIndex)

  2. Получить приблизительное среднее значение = низкое значение + (средний индекс-низкий индекс) * DistributionPerUnit

  3. Получить NumberOfItemsWhichAreLessThanorEqualToMedianValue = K

  4. есть (K = MedianIndex)? return MedianValue

  5. есть (K> MedianIndex)? тогда HighIndex = K и HighValue = MedianValue, иначе LowIndex = K и LowValue = MedianValue

Это будет быстрее, чем 1-й порядок, без использования памяти

Мы также можем подумать о подгонке HighValue, LowValue и MedianValue с HighIndex, LowIndex и MedianIndex к параболе и можем получить двоичный поиск третьего порядка, который будет быстрее, чем 2-й порядок, без использования памяти и так далее ...

person lakshmanaraj    schedule 12.03.2009
comment
Это прекрасный способ сделать это, но, пожалуйста, укажите явно, что для этого требуется несколько (log (n) в ожидании) проходов через данные, поскольку вы не будете хранить NumberOfItemsWhichAreLessThanorEqualToMedianValue [k] в ОЗУ. - person j_random_hacker; 09.04.2009

Обычно, если ввод находится в определенном диапазоне, скажем, от 1 до 1 миллиона, легко создать массив подсчетов: прочтите здесь код для quantile и ibucket: http://code.google.com/p/ea-utils/source/browse/trunk/clipper/sam-stats.cpp

Это решение можно обобщить как приближение путем принуждения ввода к целому числу в некотором диапазоне с помощью функции, которую вы затем отмените на выходе: IE: foo.push ((int) input / 1000000) и quantile (foo) * 1000000 .

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

Или вы можете использовать метод медианных троек, описанный в этой статье: http://web.cs.wpi.edu/~hofri/medsel.pdf

person Erik Aronesty    schedule 04.01.2013

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

Function QuantileIterative(Var x : Array of Double; n : Integer; p, mean, sigma : Double) : Double;
Var eta, quantile,q1, dq : Double;
    i : Integer;
Begin
  quantile:= mean + 1.25*sigma*(p-0.5);
  q1:=quantile;
  eta:=0.2*sigma/xy(1+n,0.75); // should not be too large! sets accuracy
  For i:=1 to n Do 
     quantile := quantile + eta * (signum_smooth(x[i] - quantile,eta) + 2*p - 1);
  dq:=abs(q1-quantile);
  If dq>eta
     then Begin
          If dq<3*eta then eta:=eta/4;
          For i:=1 to n Do 
             quantile := quantile + eta * (signum_smooth(x[i] - quantile,eta) + 2*p - 1);
     end;
  QuantileIterative:=quantile
end;

Поскольку медиана для двух элементов будет средним значением, я использовал сглаженную сигнум-функцию, а xy () - это x ^ y. Есть идеи, как его улучшить? Конечно, если у нас есть больше априорных знаний, мы можем добавить код, используя min и max массива, перекоса и т. Д. Для больших данных вы, возможно, не будете использовать массив, но для тестирования это проще.

person user32038    schedule 06.02.2019

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


    # find min on the fly
    if minDataPoint > dataPoint:
        minDataPoint = dataPoint  

    # find max on the fly
    if maxDataPoint < dataPoint:
        maxDataPoint = dataPoint
    
    # estimate median base on the current data
    estimate_mid = (maxDataPoint + minDataPoint) / 2
    
    #if **new** dataPoint is closer to the mid? stor it
    if abs(midDataPoint - estimate_mid) > abs(dataPoint - estimate_mid):
        midDataPoint = dataPoint    

В духе @lakshmanaraj

person Hananel    schedule 01.06.2021