Физтех.Статистика
Скачать ipynb
Python для анализа данных¶
Библиотека pandas¶
Pandas — пакет для статистической обработки данных, по функциональности близкий к SQL и R. Включает в себя функциональность работы с базами данных и таблицами Excel.
import numpy as np
import pandas as pd
import scipy.stats as sps
import warnings
warnings.simplefilter("ignore", FutureWarning)
1. Тип данных Series
¶
Одномерный набор данных. Отсутствующий данные записываются как np.nan
. Например, в этот день термометр сломался или метеоролог был пьян. При вычислении среднего и других операций соответствующие функции не учитывают отсутствующие значения.
l = [1, 3, 5, np.nan, 6, 8]
s = pd.Series(l)
s
Полезно знать: Для поиска пропусков есть специальный метод .isna()
. Он эквивалентен конструкции s != s
s.isna()
Основная информация о наборе данных: количество записей, среднее, стандартное отклонение, минимум, нижний квартиль, медиана, верхний квартиль, максимум, а так же тип данных.
s.describe()
В данном примере обычная индексация.
s[2]
s[2] = 7
s
s[2:5]
s1 = s[1:]
s1
s2 = s[:-1]
s2
В сумме s1+s2
складываются данные с одинаковыми индексами. Поскольку в s1
нет данного и индексом 0, а в s2
— с индексом 5, в s1+s2
в соответствующих позициях будет NaN
.
s1 + s2
К наборам данных можно применять функции из numpy
.
np.exp(s)
При создании набора данных s
мы не указали, что будет играть роль индекса. По умолчанию это последовательность неотрицательных целых чисел 0, 1, 2, ...
s.index
Но можно создавать наборы данных с индексом, заданным списком.
i = list('abcdef')
i
s = pd.Series(l, index=i)
s
s['c']
Если индекс — строка, то вместо s['c']
можно писать s.c
.
s.c
Набор данных можно создать из словаря.
s = pd.Series({'a':1, 'b':2, 'c':0})
s
Можно отсортировать набор данных.
s.sort_values()
Роль индекса может играть, скажем, последовательность дат или времён измерения и т.д..
d = pd.date_range('20160101', periods=10)
d
s = pd.Series(sps.norm.rvs(size=10), index=d)
s
Операции сравнения возвращают наборы булевых данных.
s > 0
Если такой булев набор использовать для индексации, получится поднабор только из тех данных, для которых условие есть True
.
s[s > 0]
Кумулятивные максимумы — от первого элемента до текущего. Первое значение кумулятивного максимума совпадает с первым значением исходного массива. Далее значение $k$-го элемента есть максимум среди элементов до $k$-го включительно.
s.cummax()
Кумулятивные суммы. Первое значение кумулятивной суммы совпадает с первым значением исходного массива. Далее значение $k$-го элемента есть сумма элементов до $k$-го включительно.
s.cumsum()
Произвольные функции кумулятивным способом можно считать с помощью конструкции expanding
. Например, так можно посчитать кумулятивные медианы. Будет не быстрее, чем вручную, но аккуратнее.
s.expanding().apply(np.median, raw=True)
Если вы хотите посчитать разности соседних элементов, воспользуйтесь методом diff
. Ключевое слово periods
отвечает за то, с каким шагом будут считаться разности.
s.diff()
Результат будет иметь тот же размер, но в начале появятся пропущенные значения. От них можно избавиться при помощи метода dropna
.
s.diff().dropna()
Упражнение
Посчитайте кумулятивное среднее квадратов разностей соседних элементов набора s
.
# ВАШ КОД
s.diff().dropna().expanding().apply(lambda x: np.mean(x**2))
Наконец, построим график.
import matplotlib.pyplot as plt
%matplotlib inline
# Нужно для новых версий библиотек для преобразования дат
from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()
plt.figure(figsize=(15, 4))
plt.plot(s)
plt.show()
Более подробно ознакомиться с методами можно в официальной документации.
2. Тип данных DataFrame
¶
Двумерная таблица данных. Имеет индекс и набор столбцов (возможно, имеющих разные типы). Таблицу можно построить, например, из словаря, значениями в котором являются одномерные наборы данных.
d = {'one': pd.Series(range(6), index=list('abcdef')),
'two': pd.Series(range(7), index=list('abcdefg')),
'three': pd.Series(sps.norm.rvs(size=7), index=list('abcdefg'))}
df = pd.DataFrame(d)
df
Таблица с несколькими разными типами данных
df2 = pd.DataFrame({ 'A': 1.,
'B': pd.Timestamp('20130102'),
'C': pd.Series(1, index=list(range(4)),
dtype='float32'),
'D': np.array([3] * 4,
dtype='int32'),
'E': pd.Categorical(["test", "train",
"test", "train"]),
'F': 'foo' })
df2
df2.dtypes
2.1 Данные¶
Вернемся к первой таблице и посмотрим на ее начало и конец
df.head()
df.tail(3)
Индексы
df.index
Названия колонок
df.columns
Получение обычной матрицы данных
df.values
Описательные статистики
df.describe()
Транспонирование данных
df.T
Сортировка по столбцу
df.sort_values(by='three', ascending=False)
Упражнение: Сгенерируйте массив точек в 3D, создайте по нему датафрейм и отсортируйте строки лексикографически.
# ВАШ КОД
pd.DataFrame(
sps.norm.rvs(size=(100, 3)),
columns=['x', 'y', 'z']
).sort_values(by=['x', 'y', 'z'])
2.2 Индексация¶
В отличии от обычной системы индексации в Python и Numpy, в Pandas принята иная система индексации, которая является несколько нелогичной, однако, на практике часто оказывается удобной при обработке сильно неоднородных данных.
Для написания продуктивного кода при обработке большого объема данных стоит использовать атрибуты .at
, .iat
, .loc
, .iloc
, .ix
.
Если в качестве индекса указать имя столбца, получится одномерный набор данных типа Series
.
df['one']
К столбцу можно обращаться как к полю объекта, если имя столбца позволяет это сделать.
df.one
Индексы полученного одномерного набора данных.
df['one'].index
У данного столбца есть имя, его можно получить следующим образом.
df['one'].name
Получение элемента массива
df['one']['c']
Правила индексации в pandas несколько отличаются от общепринятых. Если указать диапазон индексов, то это означает диапазон строк. Причём последняя строка включается в таблицу.
df['b':'d']
Диапазон целых чисел даёт диапазон строк с такими номерами, не включая последнюю строку (как обычно при индексировании списков). Всё это кажется довольно нелогичным, хотя и удобно на практике.
df[1:3]
Логичнее работает атрибут loc
: первая позиция — всегда индекс строки, а вторая — столбца.
df.loc['b']
df.loc['b', 'one']
df.loc['a':'b', 'one']
df.loc['a':'b', :]
df.loc[:, 'one']
Атрибут iloc
подобен loc
: первый индекс — номер строки, второй — номер столбца. Это целые числа, конец диапазона не включается как обычно в питоне.
df.iloc[2]
df.iloc[1:3]
df.iloc[1:3, 0:2]
Булевская индексация — выбор строк с заданным условием
df[df.three > 0]
Упражнение
Сгенерируйте случайную целочисленную матрицу $n \times m$, где $n=20, m=10$. Создайте из неё датафрейм, пронумеровав столбцы случайной перестановкой чисел из $\{1, \ldots, m\}$. Выберите столбцы с чётными номерами и строки, в которых чётных элементов больше, чем нечётных.
# ВАШ КОД
# создаем матрицу
n, m = 20, 10
data = sps.randint(low=-100, high=100).rvs(size=(n, m))
cols = np.arange(1, m + 1)
np.random.shuffle(cols)
# создаем таблицу
task_df = pd.DataFrame(data, columns=cols)
# задаем условия для строк и столбцов
col_mask = (cols % 2) == 0
row_mask = np.sum(data % 2, axis=1) < (m / 2)
# извлекаем данные по условию
task_df.loc[row_mask, col_mask]
2.3 Query¶
Pandas предлагает множество способов выбора строк из фрейма данных. Кроме функции loc
существует функция query
data = pd.DataFrame({'A': range (1,6 ),
'B': range (10,0 , -2),
'C C': range(10,5 , -1)})
data
data.query('A > B')
Предыдущее выражение эквивалентно
data[data.A > data.B]
Для столбцов с пробелами в их названиях можно использовать кавычки с обратными метками.
data.query('B == `C C`')
Предыдущее выражение эквивалентно
data[data.B == data['C C']]
2.4 Изменение таблиц¶
К таблице можно добавлять новые столбцы.
df['4th'] = df['one'] * df['two']
df['flag'] = df['two'] > 2
df
И удалять имеющиеся.
del df['two']
df['foo'] = 0
df
Изменение элемента
df.iat[1, 0] = -1
# Эквивалентные формы:
# df['one']['b'] = -1 <-- SettingWithCopyWarning
# df.at['b', 'one'] = -1
df
Добавим копию столбца one
, в которую входят только строки до третьей.
df['one_tr'] = df['one'][:3]
df
2.5 Пропуски¶
Удаление всех строк с пропусками
df.dropna(how='any')
Замена всех пропусков на значение
df.fillna(value=666)
Замена всех пропусков на среднее по столбцу
df.fillna(value=df.mean())
Булевская маска пропущенных значений
df.isnull()
2.6 Простые операции¶
Создадим таблицу из массива случайных чисел.
df1 = pd.DataFrame(sps.uniform.rvs(size=(10, 4)),
columns=['A', 'B', 'C', 'D'])
df1
df2 = pd.DataFrame(sps.uniform.rvs(size=(7, 3)),
columns=['A', 'B', 'C'])
df2
df1 + df2
2 * df1 + 3
np.sin(df1)
Построим графики кумулятивных сумм
cs = df1.cumsum()
cs
cs.plot()
plt.show()
Упражнение
Сгенерируйте случайную выборку $X_1, \ldots, X_n$ для $n = 100$ из стандартного нормального распределения, соберите из неё pd.DataFrame
, замените случайные 10% элементов на пропуски (np.nan
), а затем добавьте по столбцу для оценок первых 4 моментов кумулятивно — $$\frac{1}{m} \sum\limits_{i=1}^{m} X_i^{k}, \; i \in \overline{1, m}, \; m \in \overline{1, n}, \; k \in \overline{1, 4}$$
Ваша функция должна корректно обрабатывать пропуски. В конце постройте график.
# YOUR CODE
# генерируем выборку
n = 100
sample = sps.norm.rvs(size=n)
# создаем пропуски
index = np.random.choice(np.arange(n), int(0.1 * n), replace=True)
sample[index] = np.nan
# заводим dataframe
sample_df = pd.DataFrame(sample, columns=['Выборка'])
for k in range(1, 5):
sample_df['{}-й выборочный момент'.format(k)] = (
sample_df['Выборка'] ** k
).expanding().apply(np.nanmean)
sample_df.plot(figsize=(10, 5));
3. Чтение и запись данных¶
Загрузка текстовых файлов табличного вида производится с помощью функции pd.read_csv
. Основные аргументы следующие:
filepath_or_buffer
— пусть к файлу;sep
— разделитель колонок в строке (запятая, табуляция и т.д.);header
— номер строки или список номеров строк, используемых в качестве имен колонок;names
— список имен, которые будут использованы в качестве имен колонок;index_col
— колонка, используемая в качестве индекса;usecols
— список имен колонок, которые будут загружены;nrows
— сколько строк прочитать;skiprows
— номера строк с начала, которые нужно пропустить;skipfooter
— сколько строк в конце пропустить;na_values
— список значений, которые распознавать как пропуски;parse_dates
— распознавать ли даты, можно передать номера строк;date_parser
— парсер дат;dayfirst
— день записывается перед месяцем или после;thousands
— разделитель тысяч;decimal
— разделитель целой и дробной частей;comment
— символ начала комментария.
Полный список параметров:
pd.read_csv(filepath_or_buffer, sep=',', delimiter=None, header='infer', names=None, index_col=None, usecols=None, squeeze=False, prefix=None, mangle_dupe_cols=True, dtype=None, engine=None, converters=None, true_values=None, false_values=None, skipinitialspace=False, skiprows=None, nrows=None, na_values=None, keep_default_na=True, na_filter=True, verbose=False, skip_blank_lines=True, parse_dates=False, infer_datetime_format=False, keep_date_col=False, date_parser=None, dayfirst=False, iterator=False, chunksize=None, compression='infer', thousands=None, decimal=b'.', lineterminator=None, quotechar='"', quoting=0, escapechar=None, comment=None, encoding=None, dialect=None, tupleize_cols=False, error_bad_lines=True, warn_bad_lines=True, skipfooter=0, skip_footer=0, doublequote=True, delim_whitespace=False, as_recarray=False, compact_ints=False, use_unsigned=False, low_memory=True, buffer_lines=None, memory_map=False, float_precision=None)
Загрузка таблиц формата Excel производится с помощью функции pd.read_excel
. Основные аргументы следующие:
io
— пусть к файлу;sheetname
— какие листы таблицы загрузить;- Остальные параметры аналогично.
pd.read_excel(io, sheetname=0, header=0, skiprows=None, skip_footer=0, index_col=None, names=None, parse_cols=None, parse_dates=False, date_parser=None, na_values=None, thousands=None, convert_float=True, has_index_names=None, converters=None, dtype=None, true_values=None, false_values=None, engine=None, squeeze=False, **kwds)
Запись таблицы в текстовый файл производится с помощью функции df.to_csv
. Основные аргументы следующие:
df
— DataFrame, который нужно записать;path_or_buf
— путь, куда записать;sep
— разделитель колонок в строке (запятая, табуляция и т.д.);na_rep
— как записать пропуски;float_format
— формат записи дробных чисел;columns
— какие колонки записать;header
— как назвать колонки при записи;index
— записывать ли индексы в файл;index_label
— имена индексов, которые записать в файл.
Полный список параметров:
df.to_csv(path_or_buf=None, sep=',', na_rep='', float_format=None, columns=None, header=True, index=True, index_label=None, mode='w', encoding=None, compression=None, quoting=None, quotechar='"', line_terminator='\n', chunksize=None, tupleize_cols=False, date_format=None, doublequote=True, escapechar=None, decimal='.')
Запись таблицы в формат Excel производится с помощью функции df.to_excel
. Основные аргументы аналогичные. Полный список параметров:
df.to_excel(excel_writer, sheet_name='Sheet1', na_rep='', float_format=None, columns=None, header=True, index=True, index_label=None, startrow=0, startcol=0, engine=None, merge_cells=True, encoding=None, inf_rep='inf', verbose=True, freeze_panes=None)
4. Примеры чтения данных и работы с датами¶
Прочитаем файл, который содержит два столбца — дата и число. Столбцы разделяются табуляцией.
df = pd.read_csv('./example.csv', sep='\t', parse_dates=[0])
df.head()
В информации о таблице видим, что дата определилась, т.к. формат колонки Time
обозначен как datetime64[ns]
.
df.info()
Но при печати понимаем, что часть дат распозналась неправильно. Если число месяца меньше 13, то pandas путает день и месяц. В одном и том же столбце. Кошмар...
df['Time']
Укажем, что день всегда следует первым. Теперь все правильно
df = pd.read_csv('./example.csv', sep='\t', parse_dates=[0],
dayfirst=True)
df['Time']
Панды довольно ленивые, и если не попросить pandas распознать дату, то ничего делать не будет — оставит ее как object
.
df = pd.read_csv('./example.csv', sep='\t')
df.info()
Тогда можно воспользоваться функцией pd.to_datetime
df['Time'] = pd.to_datetime(df['Time'], dayfirst=True)
df['Time']
Установим дату как индекс, получив временной ряд.
df = df.set_index('Time')
df
Его можно нарисовать
df.plot();
Усреднение данных по трем дням
df.resample('3D').mean()
5. Интервалы времени¶
Интервал времени задается объектом pd.Timedelta
Возможные обозначения интервалов: 'Y', 'M', 'W', 'D', 'days', 'day', 'hours', hour', 'hr', 'h', 'm', 'minute', 'min', 'minutes', 'T', 'S', 'seconds', 'sec', 'second', 'ms', 'milliseconds', 'millisecond', 'milli', 'millis', 'L', 'us', 'microseconds', 'microsecond', 'micro', 'micros', 'U', 'ns', 'nanoseconds', 'nano', 'nanos', 'nanosecond', 'N'
Например, интервал времени в 5 недель 6 дней 5 часов 37 минут 23 секунды 12 милисекунд:
pd.Timedelta('5W 6 days 5hr 37min 23sec 12ms')
Поробуем понять что такое Y
и M
.
pd.Timedelta('1Y'), pd.Timedelta('1M')
Символ Y
обозначает год. Он сделан таким из-за високосных годов. Поскольку месяцы разной длины, то их вообще нельзя здесь задать. Поэтому M
обозначает минуты.
Интервал можно добавить к какой-нибудь дате, или вычесть из нее.
pd.to_datetime('2019.09.18 18:30') \
+ pd.Timedelta('8hr 37min 23sec 12ms')
pd.to_datetime('2019.09.18 18:30') \
- pd.Timedelta('20hr 50min 23sec 12ms')
Сделать регулярный список дат позволяет функция pd.timedelta_range
, которая реализует функционал range
для дат. Ей нужно передать ровно три аргумента из следующих четырех:
start
— интервал начала отчета;end
— интервал окончания отчета;periods
— количество интервалов;freq
— частота отсчета.
Пример
Врач на протяжении дня измеряет пациенту температуру каждые 3 часа в течение 2 недель. Также пациенту необходимо спать с 11 вечера до 7 утра. Каждый день измерения температуры начинаются в 8 часов. Первое измерение 22 марта 2020 года. Определите моменты времени, когда нужно измерить пациенту температуру.
# Периоды измерения температуры днем
periods = pd.timedelta_range(start='8H', freq='3H', end='23H')
periods
# Даты измерний температуры
dates = pd.to_datetime('2020.03.22') \
+ pd.timedelta_range(start=0, freq='1D', end='2W')
dates
# Время измерния температуры
n, m = len(dates), len(periods)
dates_new = dates.repeat(m)
periods_new = pd.to_timedelta(np.tile(periods, n))
time = dates_new + periods_new
time
При подготовке использованы материалы https://inp.nsk.su/~grozin/python/ и http://pandas.pydata.org/pandas-docs/stable/10min.html