Skip to content

Commit 1b9f0fd

Browse files
committed
updates based on feedback
1 parent 118a87f commit 1b9f0fd

3 files changed

Lines changed: 72 additions & 27 deletions

File tree

README.rst

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,21 @@ Accepts an open-ended set of paired lists, each expected to provide an estimate
5555
Approximating means
5656
~~~~~~~~~~~~~~~~~~~
5757

58-
Estimate a mean and approximate the margin of error. The Census Bureau guidelines do not provide instructions for
59-
approximating a mean using data from the ACS. They do provide guidance for approximating a mean with data `from the PUMS <https://www2.census.gov/programs-surveys/acs/tech_docs/pums/accuracy/2013_2017AccuracyPUMS.pdf?#>`_.
60-
Instead, we implement a simulation based approach. First the number of units in each bin is simulated (assuming a normal distribution around the
61-
estimate). Then for each unit a value within the bin is simulated (assuming a uniform distribution within each bin).
62-
Note that for quantities, such as income, the Pareto distribution is `often used <https://www2.census.gov/ces/wp/2014/CES-WP-14-21.pdf>`_ instead of the uniform distribution in the upper-most bin. We provide an option to do this (`usePareto = True`).
63-
We can then calculate the mean directly from the simulated data. We repeat the simulation many times to calculate an empirical margin of error.
58+
Estimate a mean and approximate the margin of error.
59+
60+
The Census Bureau guidelines do not provide instructions for approximating a mean using data from the ACS.
61+
They do provide guidance for approximating a mean with data `from the PUMS <https://www2.census.gov/programs-surveys/acs/tech_docs/pums/accuracy/2013_2017AccuracyPUMS.pdf?#>`_.
62+
Instead, we implement a simulation based approach.
63+
64+
First the number of units in each bin is simulated (assuming a normal distribution around the estimate). Then for each unit a value within
65+
the bin is simulated (assuming a uniform distribution within each bin). Note that for quantities, such as income, the Pareto distribution is
66+
`often used <https://www2.census.gov/ces/wp/2014/CES-WP-14-21.pdf>`_ instead of the uniform distribution in the upper-most bin. We provide an
67+
option to do this (`pareto = True`). We can then calculate the mean directly from the simulated data. We repeat the simulation many times to
68+
calculate an empirical margin of error.
69+
6470
Note that this function assumes you have a lower bound for the smallest bin and an upper bound for the largest bin. We recommend trying different
6571
lower and upper bounds to assess the sensitivity of the resulting mean to your assumptions.
6672

67-
6873
Expects a list of dictionaries that divide the full range of data values into continuous categories. Each dictionary should have four keys:
6974

7075
.. list-table::
@@ -110,7 +115,7 @@ Increasing the number of runs should increase your precision (with diminishing r
110115
>>> approximate_mean(income)
111116
(98045.44530685373, 194.54892406267754)
112117
113-
>>> approximate_mean(income, usePareto=True)
118+
>>> approximate_mean(income, pareto=True)
114119
(60364.96525340687, 58.60735554621351)
115120
116121

census_data_aggregator/__init__.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -408,12 +408,18 @@ def approximate_percentchange(pair_old, pair_new):
408408
return percent_change_estimate, percent_change_moe
409409

410410

411-
def approximate_mean(range_list, number_replicates=50, usePareto=False):
411+
def approximate_mean(range_list, simulations=50, pareto=False):
412412
"""
413-
Estimate a mean and approximate the margin of error. The Census Bureau guidelines do not provide instructions for
413+
Estimate a mean and approximate the margin of error.
414+
415+
The Census Bureau guidelines do not provide instructions for
414416
approximating a mean using data from the ACS. They do provide guidance for approximating a mean with data `from the PUMS`_.
415-
Instead, we implement a simulation based approach. Due to the stochastic nature of the simulation approach, you will need to set
416-
a seed before running this function to ensure replicability. Note that this function assumes you have a lower bound for the smallest
417+
Instead, we implement a simulation based approach.
418+
419+
Due to the stochastic nature of the simulation approach, you will need to set
420+
a seed before running this function to ensure replicability.
421+
422+
Note that this function assumes you have a lower bound for the smallest
417423
bin and an upper bound for the largest bin. We recommend trying different lower and upper bounds to assess the sensitivity of the
418424
resulting mean to your assumptions.
419425
@@ -424,8 +430,8 @@ def approximate_mean(range_list, number_replicates=50, usePareto=False):
424430
* max (int): The maximum value of the range
425431
* n (int): The number of people, households or other unit in the range
426432
* moe (float): The margin of error for n
427-
number_replicates (int): number of replicates for simulation, used to estimate margin of error
428-
usePareto (logical): use the Pareto distribution to simulate from in upper bin, otherwise use uniform, Pareto is appropriate for income
433+
simulations (int): number of simulations to run, used to estimate margin of error. Defaults to 50.
434+
pareto (logical): use the Pareto distribution to simulate from in upper bin, otherwise use uniform. Pareto is appropriate for income. Defaults to False.
429435
430436
Returns:
431437
A two-item tuple with the mean followed by the approximated margin of error.
@@ -455,7 +461,7 @@ def approximate_mean(range_list, number_replicates=50, usePareto=False):
455461
]
456462
>>> approximate_mean(income)
457463
(98045.44530685373, 194.54892406267754)
458-
>>> approximate_mean(income, usePareto=True)
464+
>>> approximate_mean(income, pareto=True)
459465
(60364.96525340687, 58.60735554621351)
460466
461467
... _from the PUMS:
@@ -465,34 +471,35 @@ def approximate_mean(range_list, number_replicates=50, usePareto=False):
465471
# Sort the list
466472
range_list.sort(key=lambda x: x['min'])
467473

468-
idx = len(range_list)
469-
470-
nb1 = range_list[idx - 2]['n'] # number in second to last bin
471-
nb = range_list[idx - 1]['n'] # number in last bin
472-
lb1 = range_list[idx - 2]['min'] # lower bound of second to last bin
473-
lb = range_list[idx - 1]['min'] # lower bound of last bin
474+
if pareto: # need shape parameter if using Pareto distribution
474475

475-
alpha_hat = (numpy.log(nb1 + nb) - numpy.log(nb)) / (numpy.log(lb) - numpy.log(lb1))
476+
nb1 = range_list[-2]['n'] # number in second to last bin
477+
nb = range_list[-1]['n'] # number in last bin
478+
lb1 = range_list[-2]['min'] # lower bound of second to last bin
479+
lb = range_list[-1]['min'] # lower bound of last bin
480+
alpha_hat = (numpy.log(nb1 + nb) - numpy.log(nb)) / (numpy.log(lb) - numpy.log(lb1)) # shape parameter for Pareto
476481

477482
simulation_results = []
478-
for i in range(number_replicates):
483+
for i in range(simulations):
479484
simulated_values = []
480485
simulated_n = []
486+
# loop through every bin except the last one
481487
for range_ in range_list[:-1]:
482488
se = range_['moe'] / 1.645 # convert moe to se
483489
nn = round(numpy.random.normal(range_['n'], se)) # use moe to introduce randomness into number in bin
484490
nn = int(nn) # clean it up
485491
simulated_values.append(numpy.random.uniform(range_['min'], range_['max'], size=(1, nn)).sum()) # draw random values within the bin, assume uniform
486492
simulated_n.append(nn)
487-
if usePareto:
488-
last = range_list[idx - 1]
493+
# a special case to handle the last bin
494+
if pareto:
495+
last = range_list[-1]
489496
se = last['moe'] / 1.645 # convert moe to se
490497
nn = round(numpy.random.normal(last['n'], se)) # use moe to introduce randomness into number in bin
491498
nn = int(nn) # clean it up
492499
simulated_values.append(numpy.random.pareto(a=alpha_hat, size=(1, nn)).sum()) # draw random values within the bin, assume uniform
493500
simulated_n.append(nn)
494501
else:
495-
last = range_list[idx - 1]
502+
last = range_list[-1]
496503
se = last['moe'] / 1.645 # convert moe to se
497504
nn = round(numpy.random.normal(last['n'], se)) # use moe to introduce randomness into number in bin
498505
nn = int(nn) # clean it up

test.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,40 @@ def test_mean(self):
248248

249249
numpy.random.seed(711355)
250250

251-
mean, moe = census_data_aggregator.approximate_mean(range_list, usePareto=True)
251+
mean, moe = census_data_aggregator.approximate_mean(range_list, pareto=True)
252+
253+
self.assertAlmostEqual(mean, 60364.96525340687, places=3)
254+
self.assertAlmostEqual(moe, 58.60735554621351, places=3)
255+
256+
def test_mean_order(self):
257+
range_list = [
258+
dict(min=50000, max=59999, n=9181800, moe=20965),
259+
dict(min=60000, max=74999, n=11818514, moe=30723),
260+
dict(min=75000, max=99999, n=14636046, moe=49159),
261+
dict(min=100000, max=124999, n=10273788, moe=47842),
262+
dict(min=125000, max=149999, n=6428069, moe=37952),
263+
dict(min=150000, max=199999, n=6931136, moe=37236),
264+
dict(min=200000, max=1000000, n=7465517, moe=42206),
265+
dict(min=0, max=9999, n=7942251, moe=17662),
266+
dict(min=10000, max=14999, n=5768114, moe=16409),
267+
dict(min=15000, max=19999, n=5727180, moe=16801),
268+
dict(min=20000, max=24999, n=5910725, moe=17864),
269+
dict(min=25000, max=29999, n=5619002, moe=16113),
270+
dict(min=30000, max=34999, n=5711286, moe=15891),
271+
dict(min=35000, max=39999, n=5332778, moe=16488),
272+
dict(min=40000, max=44999, n=5354520, moe=15415),
273+
dict(min=45000, max=49999, n=4725195, moe=16890)
274+
]
275+
numpy.random.seed(711355)
276+
# Calculate the mean and its MOE
277+
mean, moe = census_data_aggregator.approximate_mean(range_list)
278+
279+
self.assertAlmostEqual(mean, 98045.44530685373, places=3)
280+
self.assertAlmostEqual(moe, 194.54892406267754, places=3)
281+
282+
numpy.random.seed(711355)
283+
284+
mean, moe = census_data_aggregator.approximate_mean(range_list, pareto=True)
252285

253286
self.assertAlmostEqual(mean, 60364.96525340687, places=3)
254287
self.assertAlmostEqual(moe, 58.60735554621351, places=3)

0 commit comments

Comments
 (0)