Очистка данных с помощью Pandas и NumPy

Специалисты по обработке данных тратят много времени на очистку наборов данных и приведение их в форму, с которой они могут работать. Фактически, многие специалисты по данным утверждают, что начальные шаги по получению и очистке данных составляют 80% работы. Именно об этом мы и поговорим в данной главе. Но перед этим, нам необходимо установить и импортировать две библиотеки:

import pandas as pd
import numpy as np

Кроме этого, в проекте мы создаем папку Datasets, куда размещаем наши исходные файлы, с которыми будет происходить работа. В моем случае это: BL-Flickr-Images-Book.csv, olympics.csv и university_towns.txt. Проделав все эти действия, можно приступать к работе.

Удаление столбцов в DataFrame

Часто вы обнаруживаете, что не все категории данных в наборе данных вам нужны. Именно поэтому, библиотека Pandas предоставляет удобный способ удаления ненужных столбцов или строк из DataFrame с помощью функции drop(). Давайте посмотрим на простой пример, в котором мы удаляем несколько столбцов из DataFrame. В приведенных ниже примерах мы передаем относительный путь к pd.read_csv, что означает, что все наборы данных находятся в папке с именем Datasets в нашем текущем рабочем каталоге:

df = pd.read_csv('Datasets/BL-Flickr-Images-Book.csv')
print('Вывод загруженного csv файла:')
print(df.head())

Результат:

Сделав вывод нашего csv файла мы видим, что несколько столбцов предствляют собой вспомогательную информацию, которая была бы полезна для библиотеки, но не очень для описания самой книги: Edition Statement, Corporate Author, Corporate Contributors, Former owner, Engraver, Issuance type и Shelfmarks. Эту информацию мы можем удалить следующим образом:

to_drop = ['Edition Statement',
           'Corporate Author',
           'Corporate Contributors',
           'Former owner',
            'Engraver',
           'Contributors',
            'Issuance type',
           'Shelfmarks']

df.drop(to_drop, inplace=True, axis=1)
print('Вывод csv файла с удаленными столбцами:')
print(df.head())

Сначала мы определили список, который содержит имена всех столбцов, которые мы хотим удалить. Затем мы вызываем функцию drop() для нашего объекта, передавая параметр inplace как True и параметр оси как 1, что говорит Pandas об изменениях непосредственно в нашем объекте и что он должен искать значения, которые будут отброшены в столбцах объекта. Результат:

Изменение индекса фрейма данных

Индекс Pandas расширяет функциональность массивов NumPy, чтобы обеспечить более гибкое нарезание и маркировку. Во многих случаях полезно использовать однозначное идентифицирующее поле данных в качестве индекса. Давайте заменим существующий индекс в BL-Flickr-Images-Book.csv столбцом Identifier, используя set_index:

df = df.set_index('Identifier')
print(' Замена существующего индекса столбцом Identifier:')
print(df.head())

Результат:

Кроме этого, мы можем получить доступ к каждой записи простым способом с помощью loc[]. Хотя loc[] может не иметь всего этого интуитивно понятного имени, он позволяет нам выполнять индексацию на основе меток, которая представляет собой маркировку строки или записи независимо от ее положения:

print('Получение доступа к каждой записи:')
print(df.loc[206])

Результат:

Другими словами, 206 — это первая метка индекса. Ранее нашим индексом был RangeIndex: целые числа, начинающиеся с 0, аналог встроенного диапазона Python. Передав имя столбца в set_index, мы изменили индекс на значения в Identifier.

Очистка полей в данных

Пока что мы удалили ненужные столбцы и изменили индекс нашего DataFrame на что-то более разумное. В этом разделе мы очистим определенные столбцы и приведем их к единому формату, чтобы лучше понять набор данных и обеспечить согласованность. В частности, мы будем очищать дату публикации и место публикации. Давайте выведем поле, содержащее дату публикации, чтобы мы могли выполнять вычисления в будущем:

print('Вывод поля даты публикации для того, чтобы мы могли выполнять вычисления в будущем')
print(df.loc[1905:, 'Date of Publication'].head(10))

Результат:

Как известно, у конкретной книги может быть только одна дата публикации. Поэтому нам необходимо удалить лишние даты в квадратных скобках, преобразовать диапазоны дат в их «дату начала», полностью удалить даты, в которых мы не уверены и преобразовать строку nan в значение NaN NumPy. Для этого мы будем использовать следующее регулярное выражение: regex = r'^(\d{4})'. Данное выражение предназначено для поиска любых четырех цифр в начале строки, чего достаточно для нашего случая. Это необработанная строка, что является стандартной практикой с регулярными выражениями. \d представляет любую цифру, а {4} повторяет это правило четыре раза. Символ ^ соответствует началу строки, а круглые скобки обозначают группу захвата, которая сигнализирует Pandas, что мы хотим извлечь эту часть регулярного выражения. Сам код:

extr = df['Date of Publication'].str.extract(r'^(\d{4})', expand=False)
print('Модернизированные поля даты публикации:')
print(extr.head())

Результат:

Объединение методов str с NumPy для очистки столбцов

Для начала, давайте выведем содержимое столбца Place of Publication:

print('Вывод содержимого столбца Place of Publication')
print(df['Place of Publication'].head(10))

Результат:

Мы видим, что для некоторых строк место публикации окружено другой ненужной информацией. Если бы мы посмотрели на большее количество значений, мы бы увидели, что это справедливо только для некоторых строк, место публикации которых — ‘London’ или ‘Oxford’. Давайте взглянем на две конкретные записи:

print('Вывод информации о двух конкретных записях:')
print(df.loc[4157862])
print(df.loc[4159587])

Результат:

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

pub = df['Place of Publication']
london = pub.str.contains('London')
print('Вывод очищенной колонки:')
print(london[:5])
oxford = pub.str.contains('Oxford')

Далее объединяем их с помощью np.where:

df['Place of Publication'] = np.where(london, 'London',
                                      np.where(oxford, 'Oxford',
                                               pub.str.replace('-', ' ')))
print('Объединение с помощью  np.where')
print(df['Place of Publication'].head())

Результат:

Здесь функция np.where вызывается во вложенной структуре с условием, представляющим собой серию логических значений, полученных с помощью str.contains(). Метод contains() работает аналогично встроенному ключевому слову in, используемому для поиска вхождения объекта в итерируемом объекте (или подстроке в строке). Используемая замена — это строка, представляющая желаемое место публикации. Мы также заменяем дефисы пробелом с помощью str.replace() и переназначаем столбец в нашем DataFrame.

Очистка всего набора данных с помощью функции applymap

В определенных ситуациях вы увидите, что «грязь» не локализована в одном столбце, а более разбросана. В некоторых случаях было бы полезно применить настраиваемую функцию к каждой ячейке или элементу DataFrame. Метод Pandas .applymap() похож на метод in-построил функцию map() и просто применяет функцию ко всем элементам в DataFrame. Давайте посмотрим на пример. Мы создадим DataFrame из ранее добавленного в проект файла «university_towns.txt»:

$ head Datasets/univerisity_towns.txt
Alabama[edit]
Auburn (Auburn University)[1]
Florence (University of North Alabama)
Jacksonville (Jacksonville State University)[2]
Livingston (University of West Alabama)[2]
Montevallo (University of Montevallo)[2]
Troy (Troy University)[2]
Tuscaloosa (University of Alabama, Stillman College, Shelton State)[3][4]
Tuskegee (Tuskegee University)[5]
Alaska[edit]

Мы видим, что у нас есть периодические названия штатов, за которыми следуют университетские города в этом штате: StateA TownA1 TownA2 StateB TownB1 TownB2 …. Если мы посмотрим на то, как названия штатов записаны в файле, мы увидим, что все они имеют в них подстрока [edit]. Мы можем воспользоваться этим шаблоном, создав список (state, city) кортежи и обертывание этого списка в DataFrame:

university_towns = []
with open('Datasets/university_towns.txt') as file:
     for line in file:
         if '[edit]' in line:

             state = line
         else:

             university_towns.append((state, line))
print('Вывод созданного списка, преобразованного в DataFrame:')
print(university_towns[:5])

Результат:

Мы можем обернуть этот список в DataFrame и установить столбцы как «State» и «RegionName». Pandas возьмет каждый элемент в списке и установит State на левое значение, а RegionName — на правое значение:

towns_df = pd.DataFrame(university_towns,
                         columns=['State', 'RegionName'])
print('Вывод результирующего DataFrame:')
print(towns_df.head())

Результат:

Хотя мы могли бы очистить эти строки в цикле for выше, Pandas упрощает это. Нам нужно только название штата и название города, а все остальное можно удалить. Хотя здесь мы могли бы снова использовать методы Pandas .str(), мы также могли бы использовать applymap() для сопоставления вызываемого Python с каждым элементом DataFrame.

Переименование столбцов и пропуск строк

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

$ head -n 5 Datasets/olympics.csv
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
,? Summer,01 !,02 !,03 !,Total,? Winter,01 !,02 !,03 !,Total,? Games,01 !,02 !,03 !,Combined total
Afghanistan (AFG),13,0,0,2,2,0,0,0,0,0,13,0,0,2,2
Algeria (ALG),12,5,2,8,15,3,0,0,0,0,15,5,2,8,15
Argentina (ARG),23,18,24,28,70,18,0,0,0,0,41,18,24,28,70

Теперь мы прочитаем его в DataFrame Pandas:

olympics_df = pd.read_csv('Datasets/olympics.csv')
print('Вывод olympics.csv:')
print(olympics_df.head())

Результат:

Это действительно грязно! Поэтому, мы должны пропустить одну строку и установить заголовок как первую (с нулевым индексом) строку и переименовать столбцы. Для того, чтобы удалить 0-ю строку мы используем:

olympics_df = pd.read_csv('Datasets/olympics.csv', header=1)
print('Вывод olympics.csv без 0 строки:')
print(olympics_df.head())

Результат:

Теперь у нас есть правильная строка, установленная в качестве заголовка, и все ненужные строки удалены. Обратите внимание на то, как Pandas изменил имя столбца, содержащего названия стран, с NaN на Unnamed: 0. Чтобы переименовать столбцы, мы будем использовать метод rename() DataFrame, который позволяет вам изменить метку оси на основе сопоставления (в данном случае dict). Начнем с определения словаря, который сопоставляет текущие имена столбцов (как ключи) с более удобными (значениями словаря):

new_names =  {'Unnamed: 0': 'Country',
              '? Summer': 'Summer Olympics',
              '01 !': 'Gold',
               '02 !': 'Silver',
              '03 !': 'Bronze',
               '? Winter': 'Winter Olympics',
               '01 !.1': 'Gold.1',
              '02 !.1': 'Silver.1',
               '03 !.1': 'Bronze.1',
               '? Games': '# Games',
               '01 !.2': 'Gold.2',
              '02 !.2': 'Silver.2',
              '03 !.2': 'Bronze.2'}

Далее вызываем функцию rename() для нашего объекта:

olympics_df.rename(columns=new_names, inplace=True)

Установка inplace в True указывает, что наши изменения будут внесены непосредственно в объект. Результат:

Описательная статистика на Python (Метрики оценки корреляции или взаимозависимости)

В предыдущей части мы уже начали разбирать описательную статистику, а именно: центральные метрики и метрики оценки вариативности. В этой же части мы будем говорить той же описательной статистики, но уже разберем корреляции между парами данных. Корреляция – это статистическая взаимосвязь между двумя или более случайными величинами. И есть две статистические меры, которые характеризуют корреляцию между наборами данных — ковариация и коэффициент корреляции. Но прежде, чем нам начать с ними работать, необходимо установить и импортировать нужные библиотеки, а именно: math, statistics, numpy, scipy.stats, pandas и matplotlib.pyplot. После этого, мы формируем два списка Python, которые будем использовать для получения соответствующих массивов NumPy и серии Pandas:

x = list(range(-10, 11))
y = [0, 2, 2, 2, 2, 3, 3, 6, 7, 4, 7, 6, 6, 9, 4, 5, 5, 10, 11, 12, 14]
x_, y_ = np.array(x), np.array(y)
x__, y__ = pd.Series(x_), pd.Series(y_)

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

Ковариации

Выборочная ковариация — это мера, которая количественно определяет силу и направление взаимосвязи между парой переменных:
• Если корреляция положительная, то и ковариация тоже положительная. Более сильное отношение соответствует более высокой ценности ковариации.
• Если корреляция отрицательна, то и ковариация также отрицательна. Более сильное отношение соответствует более низкому (или более высокому абсолютному) значению ковариации.
• Если корреляция слабая, то ковариация близка к нулю.
Процесс расчета ковариации представлен ниже:

#Ковариация
#Расчет ковариации в чистом Python
mean_x, mean_y = sum(x) / n, sum(y) / n
cov_xy = (sum((x[k] - mean_x) * (y[k] - mean_y) for k in range(n))/ (n - 1))
print(f'Расчет ковариации в чистом Python: {cov_xy}')
#Расчет ковариации с помощью NumPy
cov_matrix = np.cov(x_, y_)
print(f'Расчет ковариационной матрицы с помощью NumPy функцией cov(): {cov_matrix}')
print(f'Проверка, что левый элемент ковариационной матрицы — это ковариация x и x или дисперсия x, '
      f'а правый элемент — ковариация y и y или дисперсия y: {x_.var(ddof=1)} и {y_.var(ddof=1)}')
cov_xy = cov_matrix[0, 1]
cov_xy2 = cov_matrix[1, 0]
print(f'Проверка, что два других элемента ковариационной матрицы равны '
      f'и представляют фактическую ковариацию между x и y: {cov_xy} и {cov_xy2}')
#Расчет ковариации с помощью Pandas
cov_xy = x__.cov(y__)
cov_xy3 = y__.cov(x__)
print(f'Расчет ковариации с помощью Pandas методом .cov(): {cov_xy} и {cov_xy3}')

Первым делом была найдена ковариация в чистом Python, где сначала необходимо найти среднее значение х и у, а затем применить математическую формулу. Но можно применить функцию cov() библиотеки NumPy, которая возвращает ковариационную матрицу, что и было сделано во втором примере. Обратите внимание, cov() имеет необязательные параметры bias (по умолчанию False) и ddof (по умолчанию None). Их значения по умолчанию подходят для получения образца ковариационной матрицы. Верхний левый элемент ковариационной матрицы — это ковариация x и x или дисперсия x. Точно так же нижний правый элемент — y и y или дисперсия y. А два других элемента ковариационной матрицы равны и представляют фактическую ковариацию между x и y. Как проверить и убедиться, что это правда, разобрано в примере. Использовав np.cov() мы получили то же значение ковариации, что и с чистым Python. Помимо этого, можно было использовать метод .cov() библиотеки Pandas, что и было сделано в третьем примере, где для одного объекта Series вызывается .cov() и передает другой объект в качестве первого аргумента.

Коэффициент корреляции

Коэффициент корреляции или коэффициент корреляции Пирсона — произведение, обозначается символом ??. Коэффициент является еще одним показателем корреляции между данными. К нему надо относиться как к стандартизированной ковариации. Вот несколько важных замечаний:
• ? > 0 указывает на положительную корреляцию.
• ? < 0 указывает на отрицательную корреляцию.
• r = 1 является максимально возможным значением ?. Это свидетельство полной линейной зависимости между переменными.
• r = −1 является минимально возможным значением ?. Это свидетельство полного отсутствия линейной зависимости между переменными.
• r ≈ 0 или когда around около нуля, означает, что корреляция между переменными отсутствует.
Процесс расчета коэффициента корреляции представлен ниже:

#Коэффициент корреляции
#Расчет коэффициента корреляции в чистом Python
var_x = sum((item - mean_x)**2 for item in x) / (n - 1)
var_y = sum((item - mean_y)**2 for item in y) / (n - 1)
std_x, std_y = var_x ** 0.5, var_y ** 0.5
r = cov_xy / (std_x * std_y)
print(f'Расчет коэффициента корреляции в чистом Python: {r}')
#Расчет коэффициента корреляции с помощью scipy.stats
r, p = scipy.stats.pearsonr(x_, y_)
print(f'Расчет коэффициента корреляции и p-value, используя функцию pearsonr() в scipy.stats: {r} и {p} ')
scipy.stats.linregress(x_, y_)
print(f'Расчет коэффициента корреляции с помощью scipy.stats.linregress(): {scipy.stats.linregress(x_, y_)}')
result = scipy.stats.linregress(x_, y_)
r = result.rvalue
print(f'Получение доступа к определенным значениям из результата linregress(), включая коэффициент корреляции, используя точечную запись: {r}')
#Расчет коэффициента корреляции с помощью Pandas
r = x__.corr(y__)
r1 = y__.corr(x__)
print(f'Расчет коэффициента корреляции методом .corr() библиотеки Pandas: {r} и {r1}')

В первом примере показано, как рассчитать это коэффициент в чистом Python. Для этого нам понадобятся средние значения (mean_x и mean_y) и стандартные отклонения (std_x, std_y) для наборов данных x и y, а также их ковариация cov_xy. Далее, этот коэффициент и значение p-value (значение, показывающее принимается или отклоняется гипотеза) были рассчитан с помощью функцию pearsonr() в scipy.stats. Первое значение - это коэффициент корреляции между x_ и x_. Второй элемент — это коэффициент корреляции между y_ и y_. Их значения равны 1,0. В третьем примере коэффициент корреляции был найдем с помощью scipy.stats.linregress(). linregress() принимает x_ и y_, вычисляет линейную регрессию и возвращает результаты — наклон и точка пересечения определяют уравнение прямой регрессии, а rvalue — коэффициент корреляции. Чтобы получить доступ к определенным значениям из результата linregress(), включая коэффициент корреляции, необходимо использовать точечную запись, что показано в примере. И в четвертом примере был использован метод .corr() библиотеки Pandas, который вызывается для одного объекта Series и передает другой объект в качестве первого аргумента.

Работа с 2D данными

Axis
В статистике очень часто работают с 2D данными. NumPy и SciPy предоставляют комплексные средства для работы с ними, а Pandas имеет специальный класс DataFrame для обработки 2D данных. И прежде, чем нам работать с ними, их необходимо создать:

#Работа с данными 2D (таблицы)
#Axis
#Создание 2D массива с помощью Numpy
a = np.array(2, 3, 1],[4, 9, 2], [8, 27, 4], [16, 1, 1], [2, 3, 1)
print(f'Вывод, созданного с помощью Numpy, 2D массива: {a}')

Теперь у нас есть набор 2D данных, который мы будем использовать в этом разделе. Можно применять к нему статистические функции и методы Python так же, как к данным 1D, используя или не используя при этом необязательный параметр axis, то есть ось. Ось может принимать любое из следующих значений:
• axis = None — расчет статистики по всем данным в массиве, как в приведенном выше примере. Такое поведение часто используется по умолчанию в NumPy.
• axis = 0 — расчет статистики для каждого столбца массива. Такое поведение часто используется по умолчанию для статистических функций SciPy.
• axis = 1 — расчет статистики для каждой строки массива.
Внизу представлен код, в котором я попробовал использовать к нашему 2D данным статистические функции и методы Python, как с параметром axis, так и без:

#Использование статистических функций и методов Python к 2d массиву с необязательным параметров axis
np.mean(a, axis=0)
a.mean(axis=1)
print(f'Вывод среднего значения 2D массива методом NumPy на оси = 0 и на оси = 1 соответствнно: {np.mean(a, axis=0)} и {a.mean(axis=1)}')
np.median(a, axis=0)
np.median(a, axis=1)
print(f'Вывод медианы 2D массива методом NumPy на оси = 0 и на оси = 1 соответствнно: {np.median(a, axis=0)} и {np.median(a, axis=1)}')
scipy.stats.gmean(a)  # Default: axis=0
print(f'Вывод среднего геометрического значения 2D массива функцией SciPy на оси = 0 : {scipy.stats.gmean(a)}')
scipy.stats.gmean(a, axis=None)
print(f'Вывод среднего геометрического значения для всего 2D массива функцией SciPy: {scipy.stats.gmean(a, axis=None)}')

DataFrames
Класс DataFrame является одним из основных типов данных Pandas. С ним очень удобно работать, потому что в нем есть метки для строк и столбцов. Сам код представлен внизу:

#DataFrames
row_names = ['first', 'second', 'third', 'fourth', 'fifth']
col_names = ['A', 'B', 'C']
df = pd.DataFrame(a, index=row_names, columns=col_names)
print(f'Вывод класса DataFrame с ранее созданным 2d массивом: {df}')
df.mean()
df.var()
print(f'Вывод среднего значения и несмещенной дисперсии для всего класса DataFrame с ранее созданным 2d массивом: {df.mean()} и {df.var()}')
df.mean(axis=1)
print(f'Вывод среднего значения класса DataFrame с ранее созданным 2d массивом по оси = 1: {df.mean(axis=1)}')
df['A']
print(f'Пример изоляции класса DataFrame с ранее созданным 2d массивом по столбцу A: {df["A"]}')
df['A'].mean()
print(f'Пример расчета среднего значения класса DataFrame с ранее созданным 2d массивом по столбцу A: {df["A"].mean()}')

Первым делом был создан DataFrame использовав при этом массив a. На практике имена столбцов имеют значение и должны быть описательными. Имена строк иногда указываются автоматически как 0, 1 и т. Д. Вы можете указать их явно с помощью параметра index, хотя вы можете свободно опускать index, если хотите. Методы DataFrame очень похожи на методы Series, хотя их поведение отличается. Если вы вызываете методы статистики Python без аргументов, то DataFrame будет возвращать результаты для каждого столбца. Если же вы хотите получить результаты для каждой строки, просто укажите параметр axis = 1. Также можно изолировать каждый столбец DataFrame и применять к нему соответствующие методы. Процесс применения статистических методов и функций Python к DataFrame представлен в примере.

Визуализация данных

В дополнение к численному описанию, такому как среднее, медиана или дисперсия, можно использовать визуальные методы для представления, описания и обобщения данных. В этом разделе вы узнаете, как представить свои данные визуально, используя библиотеку matplotlib.pyplot, которая у нас уже установлена и импортирована. matplotlib.pyplot — очень удобная и широко используемая библиотека, хотя это не единственная библиотека Python, доступная для этой цели.

Box Plots

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

#Визуализация данных
#Box Plots
np.random.seed(seed=0)
x = np.random.randn(1000)
y = np.random.randn(100)
z = np.random.randn(10)
fig, ax = plt.subplots()
ax.boxplot((x, y, z), vert=False, showmeans=True, meanline=True,
           labels=('x', 'y', 'z'), patch_artist=True,
           medianprops={'linewidth': 2, 'color': 'purple'},
           meanprops={'linewidth': 2, 'color': 'red'})
plt.show()

Первый оператор инициализирует генератора случайных чисел NumPy с помощью seed(). Поэтому при каждом запуске скрипта могут получаться одинаковые результаты. Не нужно устанавливать начальное значение и, если его не указывать, то каждый раз будут получаться разные результаты. Другие операторы создают три массива NumPy с нормально распределенными псевдослучайными числами. x относится к массиву с 1000 элементами, y имеет 100, а z содержит 10 элементов. Параметры .boxplot():
• х ваши данные.
• vert устанавливает горизонтальную ориентацию графика, когда False. Ориентация по умолчанию - вертикальная.
• showmeans показывает среднее значение ваших данных, когда True.
• meanline представляет среднее в виде линии, когда истина. Представлением по умолчанию является точка.
• labels: метки ваших данных.
• patch_artist определяет, как рисовать график.
• medianprops обозначает свойства линии, представляющей медиану.
• meanprops указывает свойства линии или точки, представляющей среднее значение.
Есть и другие параметры, но их анализ выходит за рамки данного руководства. Вы можете увидеть три сюжета. Каждый из них соответствует одному набору данных (x, y или z) и показывает следующее:
• Среднее значение — это красная пунктирная линия.
• Медиана — это фиолетовая линия.
• Первый квартиль — левый край синего прямоугольника.
• Третий квартиль — это правый край синего прямоугольника.
• Межквартильный диапазон — это длина синего прямоугольника.
• Диапазон — всё слева направо.
• Выбросы — точки слева и справа.
Сюжетная диаграмма может показывать столько информации на одном рисунке!
Результат:

Гистограммы

Гистограмма особенно полезна, когда в наборе данных содержится большое количество уникальных значений. Гистограмма делит значения из отсортированного набора данных на интервалы, также называемые ячейками. Часто все лотки имеют одинаковую ширину, но это не обязательно так. Значения нижней и верхней границ ячейки называются ребрами ячейки. Частота представляет собой одно значение, которое соответствует каждому бину. Это количество элементов набора данных со значениями между краями корзины. По договоренности, все корзины, кроме самой правой, наполовину открыты. Они включают значения, равные нижним границам, но исключают значения, равные верхним границам. Крайняя правая корзина закрыта, так как включает обе границы. Если вы разделите набор данных с ребрами 0, 5, 10 и 15, то есть три элемента:
• Первый и самый левый столбец содержит значения, большие или равные 0 и меньшие 5.
• Второй контейнер содержит значения, большие или равные 5 и меньшие 10.
• Третий и самый правый контейнер содержит значения, большие или равные 10 и меньшие или равные 15.
Сам код:

#Гистограммы
hist, bin_edges = np.histogram(x, bins=10)
fig, ax = plt.subplots()
ax.hist(x, bin_edges, cumulative=False)
ax.set_xlabel('x')
ax.set_ylabel('Frequency')
plt.show()

Функция np.histogram() — это удобный способ получить данные для гистограмм. Он берет массив с вашими данными и количеством (или ребрами) бинов и возвращает два массива NumPy:
• hist содержит частоту или количество элементов, соответствующих каждому бину.
• bin_edges содержит ребра или границы корзины.
Что рассчитывает histogram(), граф .hist() может показать графически. Первый аргумент .hist() — это последовательность с вашими данными. Второй аргумент определяет края бинов. Третий отключает возможность создания гистограммы с накопленными значениями. Результат представлен внизу:

Вы можете видеть края корзины на горизонтальной оси и частоты на вертикальной оси.
При значении аргумента cumulative = True в .hist() можно получить гистограмму с совокупным количеством элементов:

Частота первого и самого левого лотка — это количество элементов в этом лотке. Частота второго бина — это сумма количества элементов в первом и втором бинах. Другие контейнеры отрисовываются аналогично. Наконец, частота последнего и самого правого бина — это общее количество элементов в наборе данных (в данном случае 1000).

Pie Charts круговые диаграммы

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

#Pie Charts круговые диаграммы
x, y, z = 128, 256, 1024
fig, ax = plt.subplots()
ax.pie((x, y, z), labels=('x', 'y', 'z'), autopct='%1.1f%%')
plt.show()

Первым делом мы определяем данные, связанные с тремя метками. Далее, Первый аргумент .pie() — данные, а второй — последовательность соответствующих меток. autopct определяет формат относительных частот, показанных на рисунке. Результат:

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

Bar Charts

Гистограммы также иллюстрируют данные, которые соответствуют заданным меткам или дискретным числовым значениям. Они могут показывать пары данных из двух наборов данных. Элементы одного набора - это метки, а соответствующие элементы другого - их частоты. При желании они также могут отображать ошибки, связанные с частотами. Гистограмма показывает параллельные прямоугольники, называемые барами. Каждая полоса соответствует одной метке и имеет высоту, пропорциональную частоте или относительной частоте ее метки. Сам код:

#Bar Charts
x = np.arange(21)
y = np.random.randint(21, size=21)
err = np.random.randn(21)
fig, ax = plt.subplots()
ax.bar(x, y, yerr=err)
ax.set_xlabel('x')
ax.set_ylabel('y')
plt.show()

В данном коде мы использовали np.arange(), чтобы получить x или массив последовательных целых чисел от 0 до 20. Мы использовали это для представления меток. y — это массив равномерно распределенных случайных целых чисел, также от 0 до 20. Этот массив будет представлять частоты. err содержит нормально распределенные числа с плавающей точкой, которые являются ошибками. Эти значения не являются обязательными. Далее создаем гистограмму с помощью .bar(), если вам нужны вертикальные столбцы или .barh(), если вам нужны горизонтальные столбцы. Результат:

X-Y участки

Диаграмма x-y или диаграмма рассеяния представляет пары данных из двух наборов данных. Горизонтальная ось x показывает значения из набора x, а вертикальная ось y показывает соответствующие значения из набора y. При желании вы можете включить линию регрессии и коэффициент корреляции. Код построения представлен внизу:

#X-Y участки
x = np.arange(21)
y = 5 + 2 * x + 2 * np.random.randn(21)
slope, intercept, r, *__ = scipy.stats.linregress(x, y)
line = f'Regression line: y={intercept:.2f}+{slope:.2f}x, r={r:.2f}'
fig, ax = plt.subplots()
ax.plot(x, y, linewidth=0, marker='s', label='Data points')
ax.plot(x, intercept + slope * x, label=line)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.legend(facecolor='white')
plt.show()

Сначала мы генерируем два набора данных и выполняем линейную регрессию с помощью scipy.stats.linregress().Набор данных x снова является массивом с целыми числами от 0 до 20. y вычисляется как линейная функция x, искаженная некоторым случайным шумом. linregress возвращает несколько значений. Вам понадобится наклон и точка пересечения линии регрессии, а также коэффициент корреляции r. Затем применяем .plot(), чтобы получить график x-y. Результат:

Схемы Зоны активности

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

#Схемы Зоны активности
matrix = np.cov(x, y).round(decimals=2)
fig, ax = plt.subplots()
ax.imshow(matrix)
ax.grid(False)
ax.xaxis.set(ticks=(0, 1), ticklabels=('x', 'y'))
ax.yaxis.set(ticks=(0, 1), ticklabels=('x', 'y'))
ax.set_ylim(1.5, -0.5)
for i in range(2):
    for j in range(2):
        ax.text(j, i, matrix[i, j], ha='center', va='center', color='w')
plt.show()

Здесь тепловая карта содержит метки «x» и «y», а также числа из ковариационной матрицы. Результат:

Желтое поле представляет самый большой элемент из матрицы 130.34, а фиолетовое соответствует наименьшему элементу 38.5. Синие квадраты между ними связаны со значением 77,8.

Вывод

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

Описательная статистика на Python (Центральные метрики и метрики оценки вариативности)

В данной работе я познакомился с описательной статистикой. Описательная статистика — это описание и интегральные параметры наборов данных. Если говорить о метриках, то в этой части была проработана центральная метрика (которая говорит нам о центрах концентрации данных, таких как среднее, медиана и мода) и метрика вариативности данных (которая говорит о разбросе значений, таких как дисперсия и стандартное отклонение). Так как я работал в PyCharm, то первым делом я зашел в интерпретатор, установил необходимые библиотеки и вызвал их:

import math
import statistics
import numpy as np
import scipy.stats
import pandas as pd

После этого, я создал некоторые исходные данные:

x = [10.0, 2, 2.5, 5, 26.0]
x_with_nan = [10.0, 2, 2.5, math.nan, 5, 26.0]
print(f'Вывод исходных данных, которые содержатся в x:{x}')
print(f'Вывод исходных данных, которые содержатся в x_with_nan:{x_with_nan}')

Теперь у нас есть списки x и x_with_nan. Они почти одинаковы, с той разницей, что x_with_nan содержат nan значение. Важно понимать поведение процедур статистики Python, когда они сталкиваются с нечисловым значением (nan). В науке о данных пропущенные значения являются общими, и вы часто будете заменять их на nan. Теперь создаем объекты np.ndarray и pd.Series, соответствующие x и x_with_nan:

y, y_with_nan = np.array(x), np.array(x_with_nan)
z, z_with_nan = pd.Series(x), pd.Series(x_with_nan)
print(f'Вывод данных, которые содержатся в y и y_with_nan:{y}, {y_with_nan}')
print(f'Вывод данных, которые содержатся в z и в z_with_nan: {z}, {z_with_nan}')

Теперь у нас есть два массива NumPy (y и y_with_nan) и два объекта Series Pandas (z и z_with_nan). Все это — 1D последовательности значений. После формирования исходных данных, приступаем к расчету центральной метрики, а именно среднего значения:

#Рассчет средних значений
mean_1 = sum(x) / len(x)
print(f'Расчет среднего значения, используя sum и len: {mean_1}')
mean_2 = statistics.mean(x)
print(f'Расчет среднего значения, используя встроенные функции статистики Python (statistics.mean(x)): {mean_2}')
mean_3 = statistics.fmean(x)
print(f'Расчет среднего значения, используя встроенные функции статистики Python (statistics.fmean(x)): {mean_3}')
mean_4 = statistics.mean(x_with_nan)
print(f'Расчет среднего значения, который содержит значения nan, используя встроенные функции статистики Python (statistics.mean(x)): {mean_4}')
mean_5 = np.mean(y)
print(f'Расчет среднего значения, используя NumPy: {mean_5}')
np.nanmean(y_with_nan)
print(f'Расчет среднего значения с помощью NumPy, игнорируя nan: {np.nanmean(y_with_nan)}')
mean_6 = z.mean()
print(f'Расчет среднего значения объекта pd.Series: {mean_6}')

Первое среднее значение было рассчитано на чистом Python, используя sum() и len(), без импорта библиотек. Хотя это чисто и элегантно, но для расчета второго, третьего и четвертого значения были применены встроенные функции библиотеки statistics Python. При расчете пятого среднего значения была использована библиотека NumPy и функция np.mean. А шестое среднее значение было рассчитано с помощью метода .mean() библиотеки Pandas.
Далее, было рассчитано средневзвешенное значение. Средневзвешенное или также называемое средневзвешенным арифметическим или средневзвешенным значением, является обобщением среднего арифметического, которое позволяет вам определить относительный вклад каждой точки данных в результат. Сам расчет:

#Рассчет средневзвешанных значений
x = [6.0, 1, 2.5, 6, 25.0]
w = [0.1, 0.2, 0.3, 0.25, 0.15]
wmean = sum(w[i] * x[i] for i in range(len(x))) / sum(w)
print(f'Расчет средневзвешанного с помощью range: {wmean}')
wmean2 = sum(x_ * w_ for (x_, w_) in zip(x, w)) / sum(w)
print(f'Расчет средневзвешанного с помощью zip: {wmean2}')
y, z, w = np.array(x), pd.Series(x), np.array(w)
wmean3= np.average(y, weights=w)
print(f'Расчет средневзвешанного с помощью np.average для массивово NumPy или серии Pandas: {wmean3}')
o = (w * y).sum() / w.sum()
print(f'Расчет средневзвешанного с помощью поэлементного умножения w * y: {o}')
w = np.array([0.1, 0.2, 0.3, 0.0, 0.2, 0.1])
print(f'Расчет средневзвешанного для набора, который содержит nan : {(w * y_with_nan).sum() / w.sum()}')

Первое и второе средневзвешенное было рассчитано В чистом Python используя комбинацию sum() с range() и zip(). Опять же, это чистая и элегантная реализация, в которой вам не нужно импортировать какие-либо библиотеки. Однако, если у вас большие наборы данных, то NumPy, вероятно, будет лучшим решением. Можно использовать np.average(), как это сделано при расчете третьего показателя, для массивов NumPy или серии Pandas. Для расчета четвертого и пятого показателя, было использовано поэлементное умножение с методом .sum().
После этого, было рассчитано гармоническое среднее, что есть обратная величина от среднего значения обратных величин всех элементов в наборе данных:

#Гармоническое среднее
hmean = len(x) / sum(1 / item for item in x)
print(f'Расчет гармонического среднего: {hmean}')
hmean2 = statistics.harmonic_mean(x)
print(f'Расчет гармонического среднего с помощью statistics.harmonic_mean(): {hmean2}')
statistics.harmonic_mean(x_with_nan)
print(f'Расчет гармонического среднего, где есть nan: {statistics.harmonic_mean(x_with_nan)}')
statistics.harmonic_mean([1, 0, 2])
print(f'Расчет гармонического среднего, где есть 0: {statistics.harmonic_mean([1, 0, 2])}')
scipy.stats.hmean(y)
print(f'Расчет гармонического среднего с помощью  scipy.stats.hmean(): {scipy.stats.hmean(y)}')

Как обычно, первое значение было рассчитано на чистом Python. Второе, третье и четвертое значение было рассчитано с помощью функции statistics.harmonic_mean(). И последнее было рассчитано используя scipy.stats.hmean.
Следом был рассчитан среднее геометрическое:

#Среднее геометрическое
gmean = 1
for item in x:
    gmean *= item
    gmean **= 1 / len(x)
print(f'Вычисление геометрического среднего: {gmean}')
gmean2 = statistics.geometric_mean(x)
print(f'Вычисление геометрического среднего с помощью  statistics.geometric_mean(): {gmean2}')
gmean3 = statistics.geometric_mean(x_with_nan)
print(f'Вычисление геометрического среднего где есть nan: {gmean3}')
scipy.stats.gmean(y)
print(f'Вычисление геометрического среднего с помощью scipy.stats.gmean(): {scipy.stats.gmean(y)}')

Первое значение было рассчитано на чистом Python. Второе и третье значение было рассчитано с помощью функции statistics.geometric_mean(). И последнее было рассчитано используя scipy.stats.gmean.
Медиана — это средний элемент отсортированного набора данных. Расчет медианы представлен внизу:

n = len(x)
if n % 2:
    median_ = sorted(x)[round(0.5*(n-1))]
else:
    x_ord, index = sorted(x), round(0.5 * n)
    median_ = 0.5 * (x_ord[index-1] + x_ord[index])
print(f'Расчет медианы: {median_}')
median_2 = statistics.median(x)
print(f'Расчет медианы с помощью statistics.median(): {median_2}')
statistics.median_low(x[:-1])
print(f'Расчет медианы с помощью statistics.median_low: {statistics.median_low(x[:-1])}')
statistics.median_high(x[:-1])
print(f'Расчет медианы с помощью statistics.median_high {statistics.median_high(x[:-1])}')
median_2 = np.median(y)
print(f'Расчет медианы с помощью np.median: {median_2}')

Первое значение было рассчитано на чистом Python. Следующие три были найдены используя statistics.median, при этом, median_low() возвращает меньшее, а median_high() — большее среднее значение. И последняя была найдена с помощью NumPy и функции np.median().
Мода — это значение в наборе данных, которое встречается чаще всего. Если такого значения не существует, набор является мультимодальным, поскольку он имеет несколько модальных значений. Расчет моды представлен внизу:

u = [2, 3, 2, 8, 12]
mode_ = max((u.count(item), item) for item in set(u))[1]
print(f'Вычисление моды: {mode_}')
mode_2 = statistics.mode(u)
print(f'Вычисление моды с помощью statistics.mode(): {mode_2}')
mode_3 = statistics.multimode(u)
print(f'Вычисление моды с помощью statistics.multimode(): {mode_3}')
mode_4 = scipy.stats.mode(u)
print(f'Вычисление моды с помощью scipy.stats.mode(): {mode_4}')

Первое значение, как обычно, получено используя чистый Python. Вы используете u.count(), чтобы получить количество вхождений каждого элемента в u. Элемент с максимальным количеством вхождений — это мода. Обратите внимание, что вам не нужно использовать set(u). Вместо этого вы можете заменить его просто на u и повторить весь список. Второе и третье значение было вычислено с помощью statistics.mode() и statistics.multimode(). Обратите внимание, mode() вернула одно значение, а multimode() в результате вернула список. Однако, это не единственное различие между двумя функциями. Если существует более одного модального значения, то mode() вызывает StatisticsError, а multimode() возвращает список со всеми режимами. И последнее значение было найдено с помощью функции, которая возвращает объект с модальным значением и количество его повторений в наборе данных.
Центральных метрик недостаточно для описания данных. Практически всегда необходимы метрики оценки вариативности данных, которые количественно определяют разброс точек данных. И первым показателем метрики оценки вариативности данных была дисперсия. Дисперсия количественно определяет разброс данных. Численно показывает, как далеко точки данных от среднего значения. Сам расчет:

n = len(x)
mean = sum(x) / n
var_ = sum((item - mean)**2 for item in x) / (n - 1)
print(f'Оценка дисперсии на чистом Python: {var_}')
var_1= statistics.variance(x)
print(f'Оценка дисперсии с помощью statistics.variance(): {var_1}')
statistics.variance(x_with_nan)
print(f'Оценка дисперсии с помощью statistics.variance(), где есть nan: {statistics.variance(x_with_nan)}')
var_2 = np.var(y, ddof=1)
print(f'Оценка дисперсии, используя NumPy с помощью np.var(): {var_2}')
var_3 = y.var(ddof=1)
print(f'Оценка дисперсии, используя NumPy с помощью метода .var(): {var_3}')

Первый метод расчета – используя чистый Python. В целом, этого достаточно и можно правильно дать оценку дисперсии. Однако, более короткое и элегантное решение — использовать функцию statistics.variance() (как сделано это при расчете второго показателя). В результате мы получили тот же результат для дисперсии, что и в первом. И оставшиеся последние два показателя были рассчитаны используя NumPy, а именно функции np.var() и метода .var()
Далее, было рассчитано среднеквадратичное отклонение. Стандартное отклонение выборки является еще одним показателем разброса данных. Он связан с оценкой дисперсией, поскольку стандартное отклонение есть положительным квадратный корень из оценки дисперсии. Стандартное отклонение часто более удобно, чем дисперсия, потому что имеет ту же размерность, что и данные. Сам расчет:

#Среднеквадратичное отклонение
std_ = var_ ** 0.5
print(f'Расчет среднеквадратичного отклонения на чистом Python: {std_}')
std_2 = statistics.stdev(x)
print(f'Расчет среднеквадратичного отклонения с помощью  statistics.stdev(): {std_2}')
np.std(y, ddof=1)
print(f'Расчет среднеквадратичного отклонения с помощью  NumPy: {np.std(y, ddof=1)}')

После этого, было найдено смещение:

 
#Смещение
x = [8.0, 1, 2.5, 4, 28.0]
n = len(x)
mean_ = sum(x) / n
var_ = sum((item - mean_)**2 for item in x) / (n - 1)
std_ = var_ ** 0.5
skew_ = (sum((item - mean_)**3 for item in x)
       * n / ((n - 1) * (n - 2) * std_**3))
print(f'Расчет смещения на чистом Python: {skew_}')
z, z_with_nan = pd.Series(x), pd.Series(x_with_nan)
print(f'Расчет смещения с помощью Pandas: {z.skew()}')

Первый показатель, был найден, соответственно с помощью чистого Python, а второй с помощью Pandas, используя метод .skew().
Процентиль — такой элемент в наборе данных, так что p элементов в наборе данных меньше или равно его значению. Кроме того, (100 - p) элементов больше или равно этому значению. Если в наборе данных есть два таких элемента, то процентиль является их средним арифметическим. Расчет процентиля представлен внизу:

#Процентили
x = [-5.0, -1.1, 0.1, 2.0, 8.0, 12.8, 21.0, 25.8, 41.0]
print(f'Расчет процентилей с помощью  statistics.quantiles(): {statistics.quantiles(x, n=2)}')
statistics.quantiles(x, n=4, method='inclusive')
print(f"Расчет процентилей с помощью  statistics.quantiles(): {statistics.quantiles(x, n=4, method='inclusive')}")
y = np.array(x)
np.percentile(y, 5)
print(f'Нахождение 5 процентиля : {np.percentile(y, 5)}')
np.percentile(y, 95)
print(f'Нахождение 95 процентиля : {np.percentile(y, 95)}')
z, z_with_nan = pd.Series(y), pd.Series(y_with_nan)
z.quantile(0.05)
print(f'Нахождение процентиля используя метод .quantile(): {z.quantile(0.05)}')

Первый показатель был найден с помощью statistics.quantiles.В этом примере 8,0 — медиана x, а 0,1 и 21,0 — это 25-й и 75-й процентили выборки соответственно. Параметр n определяет количество результирующих процентилей с равной вероятностью, а метод определяет, как их вычислять. Следующие показатели, а именно 5 и 95 процентили, были найдены с помощью библиотеки NumPy, функции np.percentile(). И последний показатель был найдет используя метод .quantile().
Диапазон данных — это разница между максимальным и минимальным элементом в наборе данных. Эти показатели я нашел используя функцию np.ptp():

#Диапазон
np.ptp(y)
np.ptp(z)
np.ptp(y_with_nan)
np.ptp(z_with_nan)
print(f'Нахождение диапазона с помощью функции  np.ptp(): {np.ptp(y),np.ptp(z),np.ptp(y_with_nan),np.ptp(z_with_nan)}')

И последнее, что я искал – сводка описательной статистики:

#Сводка описательной статистики
result = scipy.stats.describe(y, ddof=1, bias=False)
print(f'Сводка описательной статистики с помощью  scipy.stats.describe(): {result}')
result2 = z.describe()
print(f'Сводка описательной статистики с помощью  метода .describe() в Pandas: {result2}')

Первый показатель был найден с помощью scipy.stats.describe(). В качестве первого аргумента необходимо передать набор данных, который может быть представлен массивом NumPy, списком, кортежем или любой другой подобной структурой данных. Можно опустить ddof = 1, так как это значение по умолчанию и имеет значение только при расчете дисперсии. Указано bias = False для принудительного исправления асимметрии и эксцесса статистического смещения.
description() возвращает объект, который содержит следующую описательную статистику:
• nobs — количество наблюдений или элементов в вашем наборе данных;
• minmax — кортеж с минимальными и максимальными значениями;
• mean — среднее значение;
• variance — дисперсия;
• skewness — асимметрия;
• kurtosis — эксцесс вашего набора данных.
Второй показатель был найден с помощью метода .describe() библиотеки Pandas.

Автоматизация индивидуальной нарезки файлов Excel и их распространение с помощью Pandas и Outlook

Давайте представим ситуацию, что у нас есть Excel файл, в котором находится обширное количество данных. И эти данные необходимо разделить по отдельным файлам, а затем отправить по электронной почте. Чтобы было более понятно, например, у меня есть файл, в котором я храню данные о клиентах. И по каждому клиенту мне нужно создать собственный файл, а затем отправить ему это по электронной почте. Пример файла изображен внизу:

Если проводить эту операцию вручную, то придется потратить достаточное количество своего времени, а если файл будет содержать информацию не о 10 клиентах, как в моем случае, а о 1000, то это уже будет огромной проблемой. Но ее можно будет решить с помощью Python. Первым делом, как всегда, необходимо выполнить импорт библиотек (некоторые из них необходимо будет установить через pip):

import datetime
import os
import shutil
from pathlib import Path
import pandas as pd
import win32com.client as win32

Далее, в папке с проектом мы создаем несколько папок, а именно: archive и data, в которую кладем файл с исходными данными, в моем случае – customers.xlsx и в ней же создаем папку attachments, в которую у нас будут сохраняться наши новые файлы. После этого пишем следующий код:

## Установить формат даты
today_string = datetime.datetime.today().strftime('%m%d%Y_%I%p')
today_string2 = datetime.datetime.today().strftime('%b %d, %Y')

## Установка имен папок для вложений и архивирования
attachment_path = Path.cwd() / 'data' / 'attachments'
archive_dir = Path.cwd() / 'archive'
src_file = Path.cwd() / 'data' / 'customers.xlsx'
Давайте посмотрим на основной файл данных, который нужно обработать и который является источников множества файлов для клиентов:
df = pd.read_excel(src_file)
df.head()

Следующий шаг — сгруппировать все транзакции, относящиеся к клиентам по их CUSTOMER_ID, вместе. Начнем с создания группы по CUSTOMER_ID:

customer_group = df.groupby('CUSTOMER_ID')
for ID, group_df in customer_group:
    print(ID)

Результат:

После этого, у нас есть все необходимое, чтобы создать файл Excel для каждого клиента и сохранить его в каталоге attachments для дальнейшего использования:

## Запишите каждый идентификатор, группу в отдельные файлы Excel и используйте идентификатор,
## чтобы назвать каждый файл с сегодняшней датой
attachments = []
for ID, group_df in customer_group:
    attachment = attachment_path / f'{ID}_{today_string}.xlsx'
    group_df.to_excel(attachment, index=False)
    attachments.append((ID, str(attachment)))
df2 = pd.DataFrame(attachments, columns=['CUSTOMER_ID', 'FILE'])

Результат:


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

# Отправка индивидуальных отчетов по электронной почте соответствующим получателям
class EmailsSender:
    def __init__(self):
        self.outlook = win32.Dispatch('outlook.application')

    def send_email(self, to_email_address, attachment_path):
        mail = self.outlook.CreateItem(0)
        mail.To = to_email_address
        mail.Subject = today_string2 + ' Report'
        mail.Body = """Please find today's report attached."""
        mail.Attachments.Add(Source=attachment_path)
        # Показать электронную почту
        mail.Display(True)
        # Отправка
        mail.Send()

Также, можно использовать класс EmailsSender, который позволит создавать электронные письма и прикреплять файл Excel:

email_sender = EmailsSender()
for index, row in combined.iterrows():
    email_sender.send_email(row['EMAIL'], row['FILE'])

Результат:

Ну и завершающим этапом будет перемещение всех этих созданных файлов в архивный каталог:

 
# Переместить файлы в архив
for f in attachments:
    shutil.move(f[1], archive_dir)

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

Работа с PDF-файлами в Python (часть 2)

Добавление водяного знака с помощью PyPDF2

В пакете PyPDF2 есть метод mergepage(), который использует другой PDF в качестве водяного знака или штампа. Сам PDF файл, который мы хотим, чтобы программа использовала в качестве водяного знака кладем в папку source (в моем случае это mshe-logo-512x512.pdf). Начнем с чтения первой страницы исходного PDF документа и водяного знака. Для чтения файла мы используем класс PdfFileReader(). На втором шаге эти две страницы объединяются с помощью метода mergepage(), после чего результат записывается в выходной файл. Эти три этапа — создание объекта на основе класса PdfFileWriter(), добавление объединенной страницы к этому объекту с помощью метода addPage() и запись нового контента на выходную страницу с помощью метода write(). При этом, получившийся новый PDF файл с водяным знаком появится в папке dist. Сам код:

# Добавление водяного знака в одностраничный PDF

import PyPDF2

input_file = "source/ YourFile.pdf "
output_file = "dist/ YourFile -page-drafted.pdf"
watermark_file = "source/mshe-logo-512x512.pdf"

with open(input_file, "rb") as filehandle_input:
    # читать содержимое исходного файла
    pdf = PyPDF2.PdfFileReader(filehandle_input)
    
    with open(watermark_file, "rb") as filehandle_watermark:
        # читать содержание водяного знака
        watermark = PyPDF2.PdfFileReader(filehandle_watermark)
        
        # получить первую страницу оригинального PDF
        first_page = pdf.getPage(0)
        
        # получить первую страницу водяного знака PDF
        first_page_watermark = watermark.getPage(0)
        
        # объединить две страницы
        first_page.mergePage(first_page_watermark)
        
        # создать объект записи PDF для выходного файла
        pdf_writer = PyPDF2.PdfFileWriter()
        
        # добавить страницу
        pdf_writer.addPage(first_page)
        
        with open(output_file, "wb") as filehandle_output:
            # записать файл с водяными знаками в новый файл
            pdf_writer.write(filehandle_output)

Результат:

Добавление изображения с помощью PyMuPDF

В данном коде, я добавил в мой PDF файл изображение, которое предварительно положил в папку source (в моем случае это image.jpg). Сам код:

import fitz

input_file = "source/ YourFile.pdf "
output_file = "dist/ YourFile -page-image.pdf"
barcode_file = "source/YourImage.jpg"

# определить позицию (верхний правый угол)
image_rectangle = fitz.Rect(450, 170, 550, 270)

# retrieve the first page of the PDF
file_handle = fitz.open(input_file)
first_page = file_handle[0]

# добавить изображение
first_page.insertImage(image_rectangle, filename=barcode_file)

file_handle.save(output_file)

Положение изображения определяется как rectangle (прямоугольник) методом fitz.Rect(), который требует двух пар координат — левый верхний угол (x1,y1) и правый нижний угол (x2,y2) изображения. В PyMuPDF левому верхнему углу страницы соответствуют координаты (0,0). После открытия входного файла и извлечения из него первой страницы с помощью метода insertImage() добавляется наше изображение. Этот метод требует двух параметров — позиционирование с использованием imageRectangle и имя файла изображения для вставки. С помощью метода save() измененный PDF файл сохраняется на диске. Результат:

Добавление штампов с pdfrw

pdfrw — это библиотека Python и утилита, которая читает и записывает PDF файлы. И перед тем как выполнять данное задание, эту библиотеку необходимо будет установить. После этого, мы из данного пакета импортируем три класса — PdfReader PdfWriter и PageMerge. Устанавливаем соответственно объекты чтения/записи, для доступа как к содержимому PDF, так и к нашему изображению. Для каждой страницы в исходном документе вы продолжаем создавать объекты PageMerge, к которому добавляем водяной знак и который отображается всё это с помощью метода render(). Сам код:

from pdfrw import PdfReader, PdfWriter, PageMerge

input_file = "source/ YourFile.pdf "
output_file = "dist/ YourFile-pages-image.pdf"
watermark_file = "source/mshe-logo-512x512.pdf "

# определяем объекты чтения и записи
reader_input = PdfReader(input_file)
writer_output = PdfWriter()
watermark_input = PdfReader(watermark_file)
watermark = watermark_input.pages[0]

# просматривать страницы одну за другой
for current_page in range(len(reader_input.pages)):
    merger = PageMerge(reader_input.pages[current_page])
    merger.add(watermark).render()

# записать измененный контент на диск
writer_output.write(output_file, reader_input)

Результат:

Удаление страниц с помощью PyMuPDF

Библиотека PyMuPDF включает в себя довольно много сложных методов, которые упрощают удаление страниц из файла PDF. Он позволяет вам указать либо одну страницу (метод deletePage()), либо диапазон номеров страниц (метод deletePageRange()), либо список с номерами страниц (метод select()). В следующем примере будет показано, как использовать список для выбора страниц, которые следует сохранить из исходного документа. Имейте в виду, что страницы, которые не указаны, не будут частью выходного документа. В нашем случае выходной документ содержит только первую, вторую и четвертую страницы:

import fitz

input_file = "source/YourFile.pdf "
output_file = "dist/YourFile -rearranged.pdf"


# Определите страницы для сохранения - 1, 2 и 4
file_handle = fitz.open(input_file)
pages_list = [0,1,3]

# Выберите страницы и сохраните вывод
file_handle.select(pages_list)
file_handle.save(output_file)

Результат:

Разделение четных и нечетных страниц с помощью PyPDF2

Следующий пример использует PyPDF2 и разделяет файл на четные и нечетные страницы, сохраняя четные страницы в файле Documentation-Python-even.pdf и нечетные страницы в Documentation-Python-odd.pdf. Этот скрипт Python начинается с определения двух выходных файлов, Documentation-Python-even.pdf и Documentation-Python-odd.pdf, а также соответствующие им объекты для записи pdf_writer_even и pdf_writer_odd. Затем в цикле for скрипт просматривает весь файл PDF и читает одну страницу за другой. Страницы с четными номерами страниц добавляются в поток pdf_writer_even с помощью addPage(), а нечетные номера добавляются в поток pdf_writer_odd. В конце два потока сохраняются на диск в отдельных файлах, как определено ранее. Сам код:

from PyPDF2 import PdfFileReader, PdfFileWriter

pdf_document = "source/ YourFile.pdf "
pdf = PdfFileReader(pdf_document)

# Выходные файлы для новых PDF-файлов
output_filename_even = "dist/ YourFile -even.pdf"
output_filename_odd = "dist/ YourFile -odd.pdf"

pdf_writer_even = PdfFileWriter()
pdf_writer_odd = PdfFileWriter()

# Получить досягаемую страницу и добавить ее в соответствующую
# выходной файл на основе номера страницы
for page in range(pdf.getNumPages()):
    current_page = pdf.getPage(page)
    if page % 2 == 0:
        pdf_writer_odd.addPage(current_page)
    else:
        pdf_writer_even.addPage(current_page)

# Записать данные на диск
with open(output_filename_even, "wb") as out:
     pdf_writer_even.write(out)
     print("created", output_filename_even)

# Записать данные на диск
with open(output_filename_odd, "wb") as out:
     pdf_writer_odd.write(out)
     print("created", output_filename_odd)

Работа с PDF-файлами в Python (часть 1)

В данной работе мы поработаем с PDF файлами используя Python, а именно: мы рассмотрим, как извлекать текст, изображения, разделять на страницы, найти все страницы файла, где имеется нужный нам текст, как вставлять изображения в этот файл, удалять страницы, разделять страницы на четные и нечетные. Давайте же начнем!

Извлечение текста с помощью PyPDF2 и PyMuPDF

Сначала сделаем извлечение текста двумя методами. Первый – используя библиотеку PyPDF2, а второй – PyMuPDF. Что это вообще за библиотеки? PyPDF2 – это библиотека для извлечения информации и содержимого документов, постраничного разделения документов, объединения документов, обрезки страниц и добавления водяных знаков. А PyMuPDF (известный как fitz) - привязка Python для MuPDF, который является облегченным средством просмотра PDF и XPS. Именно поэтому, первым делом мы устанавливаем эти библиотеки: pip3 install pypdf2, pip3 install pymupdf. Далее, в папке с проектом мы создаем еще три дополнительные папки: images, source и dist. Папки images и dist будем использовать для записи результатов работы своих программ, а в папке source храним исходные PDF файлы (которые надо будет заранее туда положить), сами скрипты будем хранить в корне. После всех этих действий, приступаем к извлечению текста с помощью PyPDF2:

from PyPDF2 import PdfFileReader

pdf_document = "source/YourFile.pdf"
with open(pdf_document, "rb") as filehandle:  
    pdf = PdfFileReader(filehandle)
   
    info = pdf.getDocumentInfo()
    pages = pdf.getNumPages()
    print("Количество страниц в документе: %i\n\n" % pages)
    print("Мета-описание: ", info)

    for i in range(pages):
        page = pdf.getPage(i)
        print("Стр.", i, " мета: ", page, "\n\nСодержание;\n")
        print(page.extractText())

В данном коде мы импортируем PdfFileReader, помня о том, что пакет уже установлен. Задаём имя файла из папки source, открывает документ и получаем информацию о документе, используя метод getDocumentInfo() и общее количество страниц getNumPages(). Далее в цикле for читаем каждую страницу, получаем содержимое page.extractText() и печатаем в stdout. Обратите внимание, что PyPDF2 начинает считать страницы с 0, и поэтому вызов pdf.getPage(i) при i = 0 извлекает первую страницу документа. Результат:

Если использовать библиотеку PyMuPDF, то код выполняется аналогично предыдущему методу, единственный момент заключается в том, что импортируемый модуль имеет имя fitz, что соответствует имени PyMuPDF в ранних версиях:

import fitz
pdf_document = "./source/ YourFile.pdf "
doc = fitz.open(pdf_document)
print("Исходный документ: ", doc)
print("\nКоличество страниц: %i\n\n------------------\n\n" % doc.pageCount)
print(doc.metadata)
for current_page in range(len(doc)):
    page = doc.loadPage(current_page)
    page_text = page.getText("text")
    print("Стр. ", current_page+1, "\n\nСодержание;\n")
    print(page_text)

Приятной особенностью PyMuPDF является то, что он сохраняет исходную структуру документа без изменений — целые абзацы с разрывами строк сохраняются такими же, как в PDF документе. Результат:

Извлечение изображений из PDF с помощью PyMuPDF

Переходим к изображениям. PyMuPDF упрощает извлечение изображений из документов PDF с использованием метода getPageImageList(). Скрипт, приведённый ниже, основан на примере из вики-страницы PyMuPDF и извлекает и постранично сохраняет все изображения из PDF в формате PNG. Если изображение имеет цветовое пространство CMYK, оно будет сначала преобразовано в RGB. При этом, все извлеченные изображения будут сохраняться у нас в папку images. Сам код:

import fitz

pdf_document = "source/ YourFile.pdf "
doc = fitz.open(pdf_document)

print("Исходный документ", doc)
print("\nКоличество страниц: %i\n\n------------------\n\n" % doc.pageCount)
print(doc.metadata)

page_count = 0
for i in range(len(doc)):
    for img in doc.getPageImageList(i):
        xref = img[0]
        pix = fitz.Pixmap(doc, xref)
        pix1 = fitz.Pixmap(fitz.csRGB, pix)

        page_count += 1
        pix1.writePNG("images/picture_number_%s_from_page_%s.png" % (page_count, i+1))
        print("Image number ", page_count, " writed...")
        pix1 = None

В моем случае, код извлек из PDF файла 244 изображения. И все это произошло меньше чем за минуту! Результат:

Разделение PDF файлов на страницы с помощью PyPDF2

Для этого примера, в первую очередь необходимо импортировать классы PdfFileReader и PdfFileWriter. Затем мы открываем файл PDF, создаем объект для чтения и перебираем все страницы, используя метод объекта для чтения getNumPages. Внутри цикла for мы создаем новый экземпляр PdfFileWriter, который еще не содержит страниц. Затем мы добавляем текущую страницу к нашему объекту записи, используя метод pdfWriter.addPage(). Этот метод принимает объект страницы, который мы получаем, используя метод PdfFileReader.getPage(). Следующим шагом является создание уникального имени файла, что мы делаем, используя исходное имя файла плюс слово «page» плюс номер страницы. Мы добавляем 1 к текущему номеру страницы, потому что PyPDF2 считает номера страниц, начиная с нуля. Наконец, мы открываем новое имя файла в режиме (режиме wb) записи двоичного файла и используем метод write() класса pdfWriter для сохранения извлеченной страницы на диск. Все извлеченные страницы запишутся в папку dist. Сам код:

from PyPDF2 import PdfFileReader, PdfFileWriter

pdf_document = "source/ YourFile.pdf "
pdf = PdfFileReader(pdf_document)

for page in range(pdf.getNumPages()):  
    pdf_writer = PdfFileWriter()
    current_page = pdf.getPage(page)
    pdf_writer.addPage(current_page)

    outputFilename = "dist/Computer-Vision-Resources-page-{}.pdf".format(page + 1)
    with open(outputFilename, "wb") as out:
        pdf_writer.write(out)

        print("created", outputFilename)

Результат:

Найти все страницы, где есть заданный текст

Этот скрипт довольно практичен и работает аналогично pdfgrep. Используя PyMuPDF, скрипт возвращает все номера страниц, которые содержат заданную строку поиска. Страницы загружаются одна за другой и с помощью метода searchFor() обнаруживаются все вхождения строки поиска. В случае совпадения соответствующее сообщение печатается на stdout. В моем случае я нашел все страницы, которые содержат слово “Python”.

import fitz
filename = "source/ YourFile.pdf "	
search_term = "COMPUTER VISION"  
pdf_document = fitz.open(filename)
for current_page in range(len(pdf_document)):  
    page = pdf_document.loadPage(current_page)
    if page.searchFor(search_term):
        print("%s найдено на странице %i" % (search_term, current_page+1))

Результат:

Сканер поддоменов на Python

Обнаружение поддоменов определенного веб-сайта позволяет изучить всю его доменную инфраструктуру. Что это вообще такое? Домен — это адрес сайта в интернете, а поддомен — это домен, который является частью домена более высокого уровня. Для более лучшего понимания, например, у нас есть квартира, которую хозяин, после заселения, решил разделить на отдельные зоны. (для сна, отдыха, питания и так далее) И вся квартира будет выступать у нас в качестве домена, а эти функциональные зоны – поддомены. Если искать их вручную, то на это уйдет целая вечность. Но к счастью, можно написать программу на Python, которая будет делать это за нас, используя метод полного перебора, суть которого состоит в проверке всех общих имен поддоменов конкретного домена, всякий раз, когда мы получаем ответ от сервера, то есть - индикатор того, что поддомен жив. И в данной работе, я исследую сайт GitHub.

Первым делом, нам понадобится установить библиотеку requests, с помощью команды: pip3 install requests, после чего написать код, в котором мы ее импортируем и указываем домен для поиска поддоменов:

import requests

# домен для поиска поддоменов
domain = "github.com"

Далее, я использовал текстовый файл “subdomains-1000.txt”, содержащий список из 1000 поддоменов, наличие которых мы хотим проверить. Его я закинул в папку с моим проектом, после чего был написан следующий код:

# читать все поддомены
file = open("subdomains-1000.txt")
# прочитать весь контент
content = file.read()
# разделить на новые строки
subdomains = content.splitlines()
# список обнаруженных поддоменов
discovered_subdomains = []
for subdomain in subdomains:
    # создать URL
    url = f"http://{subdomain}.{domain}"
    try:
        # если возникает ОШИБКА, значит, субдомен не существует
        requests.get(url)
    except requests.ConnectionError:
        # если поддомена не существует, просто передать, ничего не выводить
        pass
    else:
        print("[+] Обнаружен поддомен:", url)
        # добавляем обнаруженный поддомен в наш список
        discovered_subdomains.append(url)

В нем мы создаем URL-адрес, подходящий для отправки запроса, затем мы используем функцию requests.get() для получения HTTP-ответа от сервера, что вызовает исключение ConnectionError всякий раз, когда сервер не отвечает, поэтому мы обернули это в блоке try/except. Если же исключение не возникло, значит субдомен существует. Запишем все обнаруженные поддомены в файл:

# сохраняем обнаруженные поддомены в файл
with open("discovered_subdomains.txt", "w") as f:
    for subdomain in discovered_subdomains:
        print(subdomain, file=f)

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

Как автоматизировать вход в систему с помощью Selenium в Python?

Автоматизация процесса входа на какой-либо веб-сайт, используя программу, написанную на Python, очень удобна и практична в эксплуатации. Реализовывается это благодаря библиотеке Selenium WebDriver. Selenium WebDriver — это библиотека для управления браузером, которая поддерживает все основные браузеры и доступна для разных языков программирования, включая Python. И в данной работе я бы хотел использовать ее для автоматизированного входа на GitHub.

Первым делом, что нам необходимо сделать – это установить Selenium для Python, использую команду: pip3 install selenium. После этого, понадобится установить специфичный драйвер для браузера, которым мы хотим управлять. Лично я, буду использовать ChromeDriver, но вы можете использовать любой другой. Далее, необходимо открыть новый скрипт Python, инициализировать WebDriver и ввести свои учетные данные от GitHub:

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait

# Github credentials
username = "username"
password = "password"

# initialize the Chrome driver
driver = webdriver.Chrome("chromedriver")

После того, как мы загрузили и разархивировали драйвер, необходимо будет его поместить в текущий каталог, то есть, скачанный мною chromedriver.exe я помещаю в папку с проектом. Именно поэтому, я просто передаю его имя конструктору.

Поскольку мы заинтересованы в автоматизации входа в систему GitHub, то перейдем на страницу входа в Github и посмотрим на неё через кнопку f12 для того, чтобы найти её HTML элементы, а именно: идентификатор полей ввода логина и пароля, а также имя кнопки входа, необходимые для получения этих элементов в коде и их запрограммирования. Обратите внимание, что поле ввода имени пользователя/адреса электронной почты имеет id поля login_field, где поле ввода пароля имеет id password. Также кнопка отправки имеет имя commit. И приведенный ниже код переходит на страницу входа в Github, извлекает эти элементы, заполняет учетные данные и нажимает кнопку:

# перейти на страницу входа в github
driver.get("https://github.com/login")

# найти поле имени пользователя / электронной почты и отправить само имя пользователя в поле ввода
driver.find_element_by_id("login_field").send_keys(username)

# найти поле ввода пароля и также вставить пароль
driver.find_element_by_id("password").send_keys(password)

# нажмите кнопку входа в систему
driver.find_element_by_name("commit").click()

В данном коде, функция find_element_by_id() извлекает HTML элемент по его идентификатору, а метод send_keys() имитирует нажатие клавиш. Приведенный выше код заставит Chrome ввести электронное письмо и пароль, а затем нажать кнопку «Sign in».

Следующее, что нужно сделать, это определить, был ли наш вход в систему успешным. Для этого есть масса способов, но здесь мы попытаемся обнаружить ошибку при входе в систему. С помощью все той же консоли, которая вызывается клавишей f12, мы увидим, что при непрвильном вводе учетных данных, появляется новый элемент div HTML с классом «flash-error», который имеет текст «Incorrect username or password» («Неправильное имя пользователя или пароль»). Приведенный ниже код отвечает за ожидание загрузки страницы после входа в систему с помощью WebDriverWait() и проверяет наличие ошибки:

# ждем завершения состояния готовности
WebDriverWait(driver=driver, timeout=10).until(
    lambda x: x.execute_script("return document.readyState === 'complete'")
)
error_message = "Incorrect username or password."

# получаем ошибки (если есть)
errors = driver.find_elements_by_class_name("flash-error")

# при необходимости распечатать ошибки
# для e в ошибках:
#     print(e.text)
# если мы находим это сообщение об ошибке в составе error, значит вход не выполнен
if any(error_message in e.text for e in errors):
    print("[!] Login failed")
else:
    print("[+] Login successful")

Тут мы используем WebDriverWait, чтобы дождаться завершения загрузки документа, метод execute_script() выполняет Javascript в браузере, код JS возвращает document.readyState === 'complete' возвращает True, если всё хорошо и False в противном случае.

В результате всех этих действий мы получаем очень удобную программу, которая упрощает нам жизнь. Например, мы написали какую-либо программу на Python и хотим ее выгрузить на наш GitHub. И раньше, чтобы это сделать, необходимо было заходить в браузер, переходить на сайт, вводить свои учетные данные и после этого уже загружать свой новый проект в репозиторий. Теперь же, достаточно просто запустить эту программу, и она все основные действия сделает за вас, вам останется лишь загрузить свой проект.

Работа с EMail

Как читать электронные письма в Python?

В ходе моего учебного процесса на паре было опробовано приложение, которое позволяло читать мои электронные письма, их удалять, писать и отправлять кому-либо от моего имени, как единичным письмом, так и рассылкой. Все это было реализовано с помощью встроенного модуля imaplib. Что вообще такое IMAP? IMAP — это стандартный протокол Интернета, который используется почтовыми клиентами для получения сообщений электронной почты с почтового сервера.
Начать я бы хотел с чтения отправленных мне писем. Для этого, нам не нужно ничего устанавливать, все модули, которые используются в этом руководстве, являются встроенными:

import imaplib
import email
from email.header import decode_header
import webbrowser
import os

# учетные данные
username = "youremailaddress @ provider.com"
password = "yourpassword"

def clean(text):
    # чистый текст для создания папки
    return "".join(c if c.isalnum() else "_" for c in text)

В данном блоке, мы импортировали необходимые модули, а затем указали учетные данные нашей электронной почты. Позже нам понадобится функция clean(), чтобы создавать папки без пробелов и специальных символов. Далее, мы подключаемся к серверу IMAP и так как я тестирую учетную запись gmail, то я использую сервер imap.gmail.com (для другой почты, необходим, соответственно, другой сервер):

# create an IMAP4 class with SSL 
imap = imaplib.IMAP4_SSL("imap.gmail.com")
# authenticate
imap.login(username, password)

Далее, необходимо уже начинать получать электронные письма:

status, messages = imap.select("INBOX")
# количество популярных писем для получения
N = 3

# общее количество писем
messages = int(messages[0])

В данном блоке мы использовали метод imap.select(), который выбирает почтовый ящик (входящие, спам и т. Д.) и выбрали папку INBOX. Переменная messages содержит общее количество сообщений в этой папке (папке «Входящие»), а статус — это просто сообщение, которое указывает, успешно ли мы получили сообщение. Затем мы преобразовали сообщения в целое число, чтобы создать цикл for. Переменная N — это количество основных сообщений электронной почты, которые вы хотите получить, сейчас я буду использовать 3, давайте переберем каждое сообщение электронной почты, извлечем все, что нам нужно, и закончим наш код:

for i in range(messages, messages-N, -1):
    # fetch the email message by ID
    res, msg = imap.fetch(str(i), "(RFC822)")
    for response in msg:
        if isinstance(response, tuple):
            # parse a bytes email into a message object
            msg = email.message_from_bytes(response[1])
            # decode the email subject
            subject, encoding = decode_header(msg["Subject"])[0]
            if isinstance(subject, bytes):
                # if it's a bytes, decode to str
                subject = subject.decode(encoding)
            # decode email sender
            From, encoding = decode_header(msg.get("From"))[0]
            if isinstance(From, bytes):
                From = From.decode(encoding)
            print("Subject:", subject)
            print("From:", From)
            # if the email message is multipart
            if msg.is_multipart():
                # iterate over email parts
                for part in msg.walk():
                    # extract content type of email
                    content_type = part.get_content_type()
                    content_disposition = str(part.get("Content-Disposition"))
                    try:
                        # get the email body
                        body = part.get_payload(decode=True).decode()
                    except:
                        pass
                    if content_type == "text/plain" and "attachment" not in content_disposition:
                        # print text/plain emails and skip attachments
                        print(body)
                    elif "attachment" in content_disposition:
                        # download attachment
                        filename = part.get_filename()
                        if filename:
                            folder_name = clean(subject)
                            if not os.path.isdir(folder_name):
                                # make a folder for this email (named after the subject)
                                os.mkdir(folder_name)
                            filepath = os.path.join(folder_name, filename)
                            # download attachment and save it
                            open(filepath, "wb").write(part.get_payload(decode=True))
            else:
                # extract content type of email
                content_type = msg.get_content_type()
                # get the email body
                body = msg.get_payload(decode=True).decode()
                if content_type == "text/plain":
                    # print only text email parts
                    print(body)
            if content_type == "text/html":
                # if it's HTML, create a new HTML file and open it in browser
                folder_name = clean(subject)
                if not os.path.isdir(folder_name):
                    # make a folder for this email (named after the subject)
                    os.mkdir(folder_name)
                filename = "index.html"
                filepath = os.path.join(folder_name, filename)
                # write the file
                open(filepath, "w").write(body)
                # open in the default browser
                webbrowser.open(filepath)
            print("="*100)
# close the connection and logout
imap.close()
imap.logout()

Во-первых, range(messages, messages-N, -1), означает переход сверху вниз, то есть самые новые сообщения электронной почты имеют наивысший идентификационный номер, а первое сообщение электронной почты имеет идентификатор 1. Во-вторых, мы использовали метод imap.fetch(), который извлекает сообщение электронной почты по идентификатору, используя стандартный формат, указанный в RFC 822. После этого, мы анализируем байты, возвращаемые методом fetch(), на правильный объект Message и используем функцию decode_header() из email.header для декодирования темы электронного адреса в удобочитаемый юникод. После того, как мы напечатали отправителя электронной почты и тему, мы хотим извлечь тело сообщения. Мы проверяем, является ли электронное сообщение составным, а это означает, что оно состоит из нескольких частей. Например, сообщение электронной почты может содержать содержимое text/html и части text/plain, что означает, что у него есть HTML-версия и текстовая версия сообщения. Он также может содержать вложения файлов, мы обнаруживаем это по заголовку Content-Disposition, поэтому мы загружаем его в новую папку, созданную для каждого сообщения электронной почты, названного в честь темы. Объект msg, который является объектом сообщения модуля электронной почты, имеет много других полей для извлечения, в этом примере мы использовали только From и Subject, напишите msg.keys() и посмотрите доступные поля для извлечения, вы можете, например, получить дату отправки сообщения с помощью msg ["Date"]. Если же вам ничего не отправили, то он будет выводить «Писем нет».

Как удалить электронные письма в Python?

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

import imaplib
import email
from email.header import decode_header

# учетные данные
username = "youremailaddress @provider.com"
password = "yourpassword"

После этого, мы подключаемся к IMAP-серверу почтового провайдера:

# create an IMAP4 class with SSL 
imap = imaplib.IMAP4_SSL("imap.gmail.com")
# authenticate
imap.login(username, password)

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

# select the mailbox I want to delete in
# if you want SPAM, use imap.select("SPAM") instead
imap.select("INBOX")

Итак, мы используем выбор почтового ящика с помощью метода imap.select(), после чего я выбрал папку INBOX. Теперь давайте выполним поиск писем, которые мы хотим удалить, по дате:

# для получения писем после определенной даты
status, messages = imap.search(None, 'SINCE "01-JAN-2020"')

status содержит строку, указывающую, был ли поиск успешно выполнен, messages возвращаются как список строки [[ASCII] почтовых идентификаторов, разделенных пробелом, преобразуем его в список целых чисел:

# преобразовать сообщения в список адресов электронной почты
messages = messages[0].split(b' ')
Теперь давайте переберем выбранные письма и отметим их как удаленные:
for mail in messages:
    _, msg = imap.fetch(mail, "(RFC822)")
    # вы можете удалить цикл for для повышения производительности, если у вас длинный список писем
    # потому что он предназначен только для печати SUBJECT целевого электронного письма, которое нужно удалить
    for response in msg:
        if isinstance(response, tuple):
            msg = email.message_from_bytes(response[1])
            # расшифровать тему письма
            subject = decode_header(msg["Subject"])[0][0]
            if isinstance(subject, bytes):
                # if it's a bytes type, decode to str
                subject = subject.decode()
            print("Deleting", subject)
    # отметить письмо как удаленное
    imap.store(mail, "+FLAGS", "\\Deleted")

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

# навсегда удалить письма, помеченные как удаленные
# из выбранного почтового ящика (в данном случае INBOX)
imap.expunge()

# закрыть почтовый ящик
imap.close()

# выйти из аккаунта
imap.logout()

Как отправлять электронные письма с Python?

В данном разделе мы будем использовать сразу несколько модулей, а именно: smtplib, email, предназначенный для отправки различных типов вложений, таких как HTML-контент и двоичные файлы, а также библиотека BeautifulSoup (которую нужно будет заранее установить), позволяющая автоматически извлекать простой текст из HTML, не беспокоясь о регулярных выражениях.
Ну и как обычно, начинаем мы с импорта всех библиотек:

import smtplib
from email import encoders
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from bs4 import BeautifulSoup as bs

Далее, мы определяем наши параметры:

# ваши учетные данные
email = "email @ example.com"
password = "password"

# электронная почта отправителя
FROM = "from @example.com"

# адрес электронной почты получателя
TO   = "to @example.com"

# тема письма (тема)
subject = "Just a subject"

Переменные email и password — это учетные данные адреса электронной почты, который вы хотите отправить, FROM и TO — это адрес электронной почты отправителя и адрес получателя соответственно (адрес электронной почты и FROM обычно совпадают), а subject — это заголовок (или тема) письма, которое мы отправим.
Убедившись в том, что значения заданы правильно, вы можете отправить сообщения на несколько адресов электронной почты, просто используя их список переменной TO, а не единственный адрес.
Кроме того, мы будем использовать объект MIMEMultipart для одновременной передачи «альтернативы» в качестве подтипа, чтобы объединить две версии почты в одно сообщение с двумя вариантами отображения:

# инициализируем сообщение, которое хотим отправить
msg = MIMEMultipart("alternative")

# установить адрес электронной почты отправителя
msg["From"] = FROM

# установить адрес электронной почты получателя
msg["To"] = TO

# задаем тему
msg["Subject"] = subject

Также, устанавливаем адреса электронной почты From, To и subject. Создадим тело сообщения:

# установить тело письма как HTML
html = """
This email is sent using Python!
"""
# делаем текстовую версию HTML
text = bs(html, "html.parser").text

В этом примере мы установили html как простое сообщение HTML, но вы можете прочитать его из почтового HTML шаблона, например:

# установить тело письма как HTML
html = open("mail.html").read()

# делаем текстовую версию HTML
text = bs(html, "html.parser").text

Далее, мы завершаем построение сообщения и смотрим, как оно выглядит:

text_part = MIMEText(text, "plain")
html_part = MIMEText(html, "html")

# прикрепить тело письма к почтовому сообщению
# сначала прикрепите текстовую версию
msg.attach(text_part)
msg.attach(html_part)
print(msg.as_string())

Теперь, когда у нас есть письмо, готовое к отправке, нужно создать функцию, которая принимает адреса электронной почты FROM и TO, а также фактическое сообщение, которое нужно отправить, и она отправляет нам электронное письмо:

def send_mail(email, password, FROM, TO, msg):
    # инициализировать SMTP-сервер
    server = smtplib.SMTP("smtp.gmail.com", 587)

    # подключиться к SMTP-серверу в режиме TLS (безопасный) и отправить EHLO
    server.starttls()

    # войти в учетную запись, используя учетные данные
    server.login(email, password)

    # отправить электронное письмо
    server.sendmail(FROM, TO, msg.as_string())

    # завершить сеанс SMTP
    server.quit()

Итак, сначала мы подключаемся к SMTP-серверу с помощью smtplib, в этом примере мы использовали SMTP-сервер Gmail с портом 587. Затем мы переводим соединение с SMTP-сервером в режим TLS для безопасности, и, наконец, мы входим в систему, используя данные учётной записи и завершаем сеанс после отправки нашего электронного письма. Server.sendmail() выполняет всю почтовую транзакцию. У метода есть несколько аргументов: адрес, отправляющий письмо (FROM), список адресов получателей письма (если это строка, то она будет рассматриваться как список с 1 адресом) и сообщение для отправки. Вызовем только что созданную функцию:

# отправить почту
send_mail(email, password, FROM, TO, msg)

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

Конспект лекции "Рынок рекрутмента"

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

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

Кроме того, люди на рынке труда поделись на три группы: те, кого уволили, и они уже занимаются поиском новой работы; те, кто подозревает, что его в скором времени уволят, и они уже занимаются поиском новой работы; и самая большая группа – это те, кто замерли, кто не спешит принимать какой-либо выбор, они просто ждут.

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

Абсолютно каждый бизнес ощущает внушительность затрат на найм одного сотрудника. В среднем это около 4000 рублей. Но в РФ сам по себе рекрутинг по стоимости очень низок, чем в Европе. Например, то, что у нас может стоит 100 долларов, там может стоить 1000. И найм всегда имеет тенденцию удорожания (особенно в условиях дефицита), потому что конкуренция уже идет на международном рынке из-за интернета, а не только на российском. И если компания хочет конкурировать, то надо инвестировать. Но можно и удешевить эту стоимость за счет ведения кадровых резервов, сотрудничества с университетами, открытия своих обучающих школ и так далее. Также, самыми востребованными навыки являются: самоорганизация, вливание в команду и самая важная - ответственность за свою работу.

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