NumPy or SciPy to calculate weighted median

numpy, python, sas-jmp, scipy, statistics

Solution

Since this is the top hit on Google for weighted median in NumPy, I will add my minimal function to select the weighted median from two arrays without changing their contents, and with no assumptions about the order of the values (on the off-chance that anyone else comes here looking for a quick recipe for the same exact pre-conditions).

def weighted_median(values, weights):
    i = np.argsort(values)
    c = np.cumsum(weights[i])
    return values[i[np.searchsorted(c, 0.5 * c[-1])]]

Using `argsort` lets us maintain the alignment between the two arrays without changing or copying their content. It should be straight-forward to extend it to an arbitrary number of arbitrary quantiles.

Update

Since it may not be fully obvious at first blush exactly how easy it is to extend to arbitrary quantiles, here is the code:

def weighted_quantiles(values, weights, quantiles=0.5):
    i = np.argsort(values)
    c = np.cumsum(weights[i])
    return values[i[np.searchsorted(c, np.array(quantiles) * c[-1])]]

This defaults to median, but you can pass in any quantile, or a list of quantiles. The return type is equivalent to what you pass in as `quantiles`, with lists promoted to NumPy arrays. With enough uniformly distributed values, you can indeed approximate the input poorly:

>>> weighted_quantiles(np.random.rand(10000), np.random.rand(10000), [0.01, 0.05, 0.25, 0.50, 0.75, 0.95, 0.99])
array([0.01235101, 0.05341077, 0.25355715, 0.50678338, 0.75697424,0.94962936, 0.98980785])
>>> weighted_quantiles(np.random.rand(10000), np.random.rand(10000), 0.5)
0.5036283072043176
>>> weighted_quantiles(np.random.rand(10000), np.random.rand(10000), [0.5])
array([0.49851076])

Update 2

In small data sets where the median/quantile is not actually observed, it may be important to be able to interpolate a point between two observations. This can be fairly easily added by calculating the mid point between two values in the case where the weight mass is equally (or quantile/1-quantile) divided between them. Due to the need for a conditional, this function always returns a NumPy array, even when `quantiles` is a single scalar. The inputs also need to be NumPy arrays now (except `quantiles` that may still be a single number).

def weighted_quantiles_interpolate(values, weights, quantiles=0.5):
    i = np.argsort(values)
    c = np.cumsum(weights[i])
    q = np.searchsorted(c, quantiles * c[-1])
    return np.where(c[q]/c[-1] == quantiles, 0.5 * (values[i[q]] + values[i[q+1]]), values[i[q]])

This function will fail with arrays smaller than 2 (the original would handle non-empty arrays).

>>> weighted_quantiles_interpolate(np.array([2, 1]), np.array([1, 1]), 0.5)
array(1.5)

Note that this extension is fairly unlikely to be needed when working with actual data sets where we typically have (a) large data sets, and (b) real-valued weights that make the odds of ending up exactly at a quantile edge very long, and probably due to rounding errors when it does happen. Including it for completeness nonetheless.

Problem

I'm trying to automate a process that JMP does (Analyze->Distribution, entering column A as the "Y value", using subsequent columns as the "weight" value). In JMP you have to do this one column at a time - I'd like to use Python to loop through all of the columns and create an array showing, say, the median of each column. For example, if the mass array is [0, 10, 20, 30], and the weight array for column 1 is [30, 191, 9, 0], the weighted median of the mass array should be 10. However, I'm not sure how to arrive at this answer. So far I've - imported the csv showing the weights as an array, masking values of 0, and - created an array of the "Y value" the same shape and size as the weights array (113x32). I'm not entirely sure I need to do this, but thought it would be easier than a for loop for the purpose of weighting. I'm not sure exactly where to go from here. Basically the "Y value" is a range of masses, and all of the columns in the array represent the number of data points found for each mass. I need to find the median mass, based on the frequency with which they were reported. I'm not an expert in Python or statistics, so if I've omitted any details that would be useful let me know! Update: here's some code for what I've done so far: ``` #Boilerplate & Import files import csv import scipy as sp from scipy import stats from scipy.stats import norm import numpy as np from numpy import genfromtxt import pandas as pd import matplotlib.pyplot as plt inputFile = '/Users/cl/prov.csv' origArray = genfromtxt(inputFile, delimiter = ",") nArray = np.array(origArray) dimensions = nArray.shape shape = np.asarray(dimensions) #Mask values ==0 maTest = np.ma.masked_equal(nArray,0) #Create array of masses the same shape as the weights (nArray) fieldLength = shape[0] rowLength = shape[1] for i in range (rowLength): createArr = np.arange(0, fieldLength*10, 10) nCreateArr = np.array(createArr) massArr.append(nCreateArr) nCreateArr = np.array(massArr) nmassArr = nCreateArr.transpose() ```

Original source