Эмпирические данные, полученные в результате измерений, часто имеют такое неприятное свойство как наличие пропусков. Обычно им присваивают значения, которые явно не имеют никакого смысла для этих данных. Так для величин количества осадков или температуры воздуха значения -999 выходят за рамки их естественной изменчивости. Часто пропуски заполняют именно такими значениями типа -999.9. Однако при расчёте различных статистических параметров (выборочное среднее, выборочная дисперсия и стандартное отклонение) наличие таких значений будет приводить к сильному искажению. Такие значения (их часто называют undefined values) необходимо фильтровать. Numpy предлагает изящный способ для решения такой задачи - масочные массивы или массивы-маски (masked arrays).
Описание masked array находится здесь -> http://docs.scipy.org/doc/numpy/reference/routines.ma.html
Примеры использования массивов-масок -> http://docs.scipy.org/doc/numpy/reference/maskedarray.generic.html#data-with-a-given-value-representing-missing-data
Продемонстрируем возможности этих массивов на следующих примерах.
Пусть дана выборка z псевдослучайной величины Z. Она изменяется в пределах [0,1). Искусственно внесём в неё значения, которые на порядок больше характерных значений (здесь прибавлялось 50.0). В реальности это могут быть ошибки наблюдений, ошибки передачи данных, ошибки вычислений и ошибки другого рода. Также это могут быть пропуски в данных.
import numpy as np
import matplotlib.pyplot as plt
NN = 100
z = np.random.random(NN)
z[z<0.3] = z[z<0.3] + 50.0
ii = np.where(z > 50.0)
print('Undefined values:',z[ii])
('Undefined values:', array([ 50.2566024 , 50.01975112, 50.27686163, 50.10802389, 50.20053467, 50.08686618, 50.23594479, 50.06535428, 50.26383849, 50.07058549, 50.27683058, 50.29247642, 50.24023024, 50.29621324, 50.11360462, 50.19390434, 50.06396334, 50.24955479, 50.10932679, 50.00275559, 50.02837202, 50.02712106, 50.07807788, 50.28143134, 50.10331262, 50.12139954, 50.1780714 , 50.15934063, 50.07576644, 50.19788189, 50.2581496 ]))
Для псевдослучайной величины Z значения больше 1 являются "лишними", вбросами/выбросами. Рассчитаем среднее значений и стандартное отклонение без фильтрации.
zm1 = np.mean(z)
zs1 = np.std(z)
print('Mean=',zm1,'Std=',zs1)
('Mean=', 16.002037955287626, 'Std=', 22.895577983165108)
Получилось, что среднее и стандартное отклонение выборки z совсем не характеризуют изначальную выборку. Даже если выпадающий элемент будет один, но он будет на порядки отличаться от характерных значений (а ещё хуже, если при этом он будет иметь другой знак), то это испортит всю статистику. Такие значений необходимо фильтровать.
zm2 = np.mean(z[z<1.0]) # Для функции numpy.mean даётся часть выборки z, где значения меньше единицы. Аналогично с numpy.std.
zs2 = np.std(z[z<1.0])
print('Filtered mean=',zm2,'Filtered std=',zs2)
('Filtered mean=', 0.65611084350012427, 'Filtered std=', 0.21865313456304422)
Аналогичный результат даёт другая форма записи кода (в стиле процедурных языков типа Фортран), которая НЕ РЕКОМЕНДУЕТСЯ для python.
n = 0
zsum = 0.0
zstd = 0.0
for i in range(len(z)):
if(z[i]<1.0):
zsum = zsum + z[i]
zstd = zstd + z[i]*z[i]
n = n + 1
zmean = zsum/n
zstd = zstd/n - zmean**2
zstd = np.sqrt(zstd)
print('Zmean=',zmean,'Zstd=',zstd)
('Zmean=', 0.65611084350012427, 'Zstd=', 0.21865313456304444)
Масочный массив, массив-маска или массив с маской - это особый класс, который отличный от класса numpy.ndarray. С помощью массива-маски предыдущая задача решается так:
# Первый способ
maska = np.ma.array(z, mask = (z < 1.0), copy = True) # Копируем в массив-маску maska массив z с условием маски
maskm = np.ma.mean(maska)
masks = np.ma.std(maska)
print('1way: Masked mean=',zm2,'Masked std=',zs2)
# Или так (второй способ)
zm2 = np.ma.masked_outside(z, 0.0, 1.0).mean() # из выборки z отбираются значения в интервале [0,1]
zs2 = np.ma.masked_outside(z, 0.0, 1.0).std()
print('2way: Masked mean=',zm2,'Masked std=',zs2)
('1way: Masked mean=', 0.65611084350012427, 'Masked std=', 0.21865313456304422) ('2way: Masked mean=', 0.65611084350012427, 'Masked std=', 0.21865313456304422)
Если необходимо считать данные из текстового файла, то можно сразу считывать в массив-маску, а не просто в массив. Такие возможности даёт функция numpy.genfromtxt.
#Так выглядит файл sample.txt
12 ; 14
32 ; 5
4 ; 64
23 ; 21
-99.9 ; 4
43 ; 85
-99.9 ; 36
-99.9 ; 94
-99.9 ; 1
213 ; 556
43 ; -9
123 ; 5
87 ; -9
94 ; 51
34 ; 12
-99.9 ; 87
# считываем значения в массив (если в файле есть пропуски-пробелы, то они не будут пропущены, а заменены на filling_values)
d1 = np.genfromtxt('sample.txt',usecols=[0],unpack= True,filling_values= -99.9) # текстовый файл с данными
print('Array1 type: ',type(d1))
print(d1)
print('Mean d1=',np.mean(d1))
# считываем значения в массив-маску (usemask=True,missing_values= -99.9).
# В качестве маски(то есть фильтра "лишних" значений) задано число -99.9
# N.B. Необходимо точно указывать число (то есть -99.9, а не просто -99 или 12.0, а не просто 12)
d2 = np.genfromtxt('sample.txt',usecols=[0],unpack= True,usemask=True,missing_values= -99.9)
print('Array2 type: ',type(d2))
print(d2)
print('Mean d2=',np.mean(d2))
('Array1 type: ', <type 'numpy.ndarray'>) [ 12. 32. 4. 23. -99.9 43. -99.9 -99.9 -99.9 213. 43. 123. 87. 94. 34. -99.9] ('Mean d1=', 13.031249999999998) ('Array2 type: ', <class 'numpy.ma.core.MaskedArray'>) [12.0 32.0 4.0 23.0 -- 43.0 -- -- -- 213.0 43.0 123.0 87.0 94.0 34.0 --] ('Mean d2=', 64.36363636363636)
Если в текстовом файле есть незаполненные пропуски, то есть просто пробелы в таблице данных, то они будут заполнены на величину missing_values. Если же она не определена, то будет использоваться значение по умолчанию ('nan' для float). Если missing_values указано явно, то пропуски-пробелы не будут замеяться, и следует ожидать неправильного считывания данных при их (пропусках) наличии. Рекомендую сначала считать данные и заполнить пробелы-пропуски, указав filling_values, а затем ещё раз наложить маску, если в этом есть необходимость.
#Так выглядит файл sample2.txt
12 , 14
32 , 5
, 64
23 , 21
-99.9 , 4
43 , 85
-99.9 , 36
, 94
-99.9 , 1
213 , 556
43 , -9
123 , 5
87 , -9
d3 = np.genfromtxt('sample2.txt',usecols=[0],unpack= True,usemask=True,missing_values= -99.9, filling_values = -99.9)
print('Array2 type: ',type(d3))
print(d3)
('Array2 type: ', <class 'numpy.ma.core.MaskedArray'>) [12.0 32.0 -99.9 23.0 -- 43.0 -- -99.9 -- 213.0 43.0 123.0 87.0]
Часто нужно получить вектор аномалий относительно среднего. С массивами-масками это делается так:
print(type(d2))
d2anom = d2.anom()
print('Anomalies array:', d2anom)
<class 'numpy.ma.core.MaskedArray'> ('Anomalies array:', masked_array(data = [-52.36363636363636 -32.36363636363636 -60.36363636363636 -41.36363636363636 -- -21.36363636363636 -- -- -- 148.63636363636363 -21.36363636363636 58.63636363636364 22.63636363636364 29.63636363636364 -30.36363636363636 --], mask = [False False False False True False True True True False False False False False False True], fill_value = 1e+20) )
Также с помошью массивов-масок можно быстро заполнить False-значения маски. Например, средними значениями.
print('Mean d2=',np.mean(d2)) # среднее
print('d2 array: ',d2)
d3 = d2.filled(np.mean(d2))
print('Gaps are filled by mean values:',d3)
print(type(d3),np.mean(d3))
('Mean d2=', 64.36363636363636) ('d2 array: ', masked_array(data = [12.0 32.0 4.0 23.0 -- 43.0 -- -- -- 213.0 43.0 123.0 87.0 94.0 34.0 --], mask = [False False False False True False True True True False False False False False False True], fill_value = 1e+20) ) ('Gaps are filled by mean values:', array([ 12. , 32. , 4. , 23. , 64.36363636, 43. , 64.36363636, 64.36363636, 64.36363636, 213. , 43. , 123. , 87. , 94. , 34. , 64.36363636])) (<type 'numpy.ndarray'>, 64.36363636363636)
Как видно из типа заполненного массива - это обычный массив, а не массив-маска. Но, конечно, на него можно опять наложить маску, снова превратив его в массив-маску.
maska2 = np.ma.array(d2, mask = (d2 > 80.0), copy = True)
print(type(maska2))
print(maska2)
<class 'numpy.ma.core.MaskedArray'> [12.0 32.0 4.0 23.0 -- 43.0 -- -- -- -- 43.0 -- -- -- 34.0 --]