Сегодня ,в первую очередь, я стал изучать продукцию данного предприятия.Смотрел какая продукция имеет наибольший и наименьший спрос.
29.06.21
Сегодня мой первый день на производственной практике . Мой руководитель по практике провёл мне обязательную инструкцию по безопасности труда. А так же познакомил меня с коллективом , с которым я буду работать. Моя задача на сегодня - вникать в предстоящую работу и наблюдать за работой коллег.
28.06.21
Сегодня мы собирались в университете всей группой , для того, что бы нам выдали документы для предстоящей практики . Нам объяснили все тонкости ее выполнения , а так же настроили нас на ответственное ее прохождение .
Разработка интерфейсов визуализации данных на Python с помощью Dash
Прежде чем приступить к выполнению работы, давайте разберем, что же такое Dash? Dash — это платформа с открытым исходным кодом для создания интерфейсов визуализации данных, помогающая специалистам по обработке данных создавать аналитические веб-приложения, не требуя передовых знаний в области веб-разработки.
Я всегда работаю в PyCharm, поэтому, прежде чем приступить к работе, я создаю проект, в котором создается виртуальная среда. Далее, я добавляю в проект файл, с которым будет происходить работа, а именно: avocado.csv, содержащий набор данных о продажах и ценах на авокадо в США за период с 2015 по 2018 годы. Скачать данный файл можно здесь. После этого, необходимо установить следующие библиотеки: dash и pandas. Теперь мы можем приступать к работе!
Создание первого приложения
Давайте создадим пустой файл с именем app1.py в корневом каталоге нашего проекта и создадим наше первое приложение:
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
data = pd.read_csv("avocado.csv")
data = data.query("type == 'conventional' and region == 'Albany'")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)
app = dash.Dash(__name__)
app.layout = html.Div(
children=[
html.H1(
children="Avocado Analytics",
style={"fontSize": "48px", "color": "red"},
),
html.P(
children="Analyze the behavior of avocado prices"
" and the number of avocados sold in the US"
" between 2015 and 2018",
),
dcc.Graph(
figure={
"data": [
{
"x": data["Date"],
"y": data["AveragePrice"],
"type": "lines",
},
],
"layout": {"title": "Average Price of Avocados"},
},
),
dcc.Graph(
figure={
"data": [
{
"x": data["Date"],
"y": data["Total Volume"],
"type": "lines",
},
],
"layout": {"title": "Avocados Sold"},
},
),
]
)
if __name__ == "__main__":
app.run_server(debug=True)
В строках с 1 по 4 мы импортируем необходимые библиотеки: dash, dash_core_components, dash_html_components и pandas. Каждая библиотека предоставляет стандартный блок для вашего приложения:
• dash поможет нам инициализировать ваше приложение.
• dash_core_components позволяет создавать интерактивные компоненты, такие как графики, раскрывающиеся списки или диапазоны дат.
• dash_html_components позволяет получить доступ к тегам HTML.
• pandas помогает читать и систематизировать данные.
В строках с 6 по 9 мы считываем данные и предварительно обрабатываем их для использования на панели управления. Мы фильтруем некоторые данные, потому что текущая версия нашей информационной панели не является интерактивной и в противном случае значения на графике не имели бы смысла. В строке 11, мы создаем экземпляр класса Dash. Если вы раньше использовали Flask, то инициализация класса Dash может показаться вам знакомой. В Flask вы обычно инициализируете приложение WSGI с помощью Flask(__name__). Точно так же для приложения Dash вы используете Dash(__name__).
Далее, мы определяем свойство макета нашего приложения, которое определяет внешний вид нашего приложения. В нашем случае мы будем использовать заголовок с описанием под ним и двумя графиками В строках с 13 по 23 вы можете увидеть на практике компоненты Dash HTML. Мы начинаем с определения родительского компонента, html.Div. Затем мы добавляем еще два элемента, заголовок (html.H1) и абзац (html.P) в качестве его дочерних элементов. Эти компоненты эквивалентны div, HTML-теги h1 и p. Вы можете использовать аргументы компонентов для изменения атрибутов или содержимого тегов. Например, чтобы указать, что находится внутри тега div, мы используем аргумент children в html.Div. В компонентах также есть другие аргументы, такие как style, className или id, которые относятся к атрибутам тегов HTML. При этом, если вы хотите изменить размер и цвет элемента H1, то достаточно прописать изменения через style, как это сделал я в 17 строке.
В строках 24–50 фрагмента кода макета вы можете увидеть компонент графика из Dash Core Components на практике. В app.layout есть два компонента dcc.Graph. Первый отображает средние цены на авокадо за период исследования, а второй — количество авокадо, проданных в США за тот же период. Под капотом Dash использует Plotly.js для создания графиков. Компоненты dcc.Graph ожидают объект фигуры или словарь Python, содержащий данные графика и макет. В этом случае вы предоставляете последнее.
И последние две строчки кода позволяют запускать приложение Dash локально, используя встроенный сервер Flask. Далее, мы запускаем наше приложение, после чего переходим по высветившейся ссылке и смотрим результат:
Создание второго приложения с добавлением внешних ресурсов
В данной главе вы узнаете, как настроить внешний вид панели инструментов, а именно:
• Добавление значка и заголовка на страницу.
• Изменение семейства шрифтов вашей панели инструментов.
• Использование внешних файлов CSS для стилизации компонентов Dash.
Сначала, в папке с проектом, мы создаем еще одну папку с именем assets, в которую добавляем скаченный значок с именем favicon.ico (скачать его можно здесь) и создаем файл style.css, содержащий следующие команды:
body {
font-family: "Lato", sans-serif;
margin: 0;
background-color: #F7F7F7;
}
.header {
background-color: #222222;
height: 256px;
display: flex;
flex-direction: column;
justify-content: center;
}
.header-emoji {
font-size: 48px;
margin: 0 auto;
text-align: center;
}
.header-title {
color: #FFFFFF;
font-size: 48px;
font-weight: bold;
text-align: center;
margin: 0 auto;
}
.header-description {
color: #CFCFCF;
margin: 4px auto;
text-align: center;
max-width: 384px;
}
.wrapper {
margin-right: auto;
margin-left: auto;
max-width: 1024px;
padding-right: 10px;
padding-left: 10px;
margin-top: 32px;
}
.card {
margin-bottom: 24px;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}
Файлы в папке assets содержат стили, которые мы применим к компонентам в макете приложения. После запуска сервера, Dash автоматически обслуживает файлы, расположенные в этой папке. И для установки значка по умолчанию нам не нужно предпринимать никаких дополнительных действий. Для использования стилей, определенных в style.css, нам необходимо использовать аргумент className в компонентах Dash. Поэтому, давайте создадим новый файл app2.py, в котором пропишем следующий код:
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
data = pd.read_csv("avocado.csv")
data = data.query("type == 'conventional' and region == 'Albany'")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)
external_stylesheets = [
{
"href": "https://fonts.googleapis.com/css2?"
"family=Lato:wght@400;700&display=swap",
"rel": "stylesheet",
},
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Avocado Analytics: Understand Your Avocados!"
app.layout = html.Div(
children=[
html.Div(
children=[
html.P(children="🥑", className="header-emoji"),
html.H1(
children="Avocado Analytics", className="header-title"
),
html.P(
children="Analyze the behavior of avocado prices"
" and the number of avocados sold in the US"
" between 2015 and 2018",
className="header-description",
),
],
className="header",
),
html.Div(
children=[
html.Div(
children=dcc.Graph(
id="price-chart",
config={"displayModeBar": False},
figure={
"data": [
{
"x": data["Date"],
"y": data["AveragePrice"],
"type": "lines",
"hovertemplate": "$%{y:.2f}"
"",
},
],
"layout": {
"title": {
"text": "Average Price of Avocados",
"x": 0.05,
"xanchor": "left",
},
"xaxis": {"fixedrange": True},
"yaxis": {
"tickprefix": "$",
"fixedrange": True,
},
"colorway": ["#17B897"],
},
},
),
className="card",
),
html.Div(
children=dcc.Graph(
id="volume-chart",
config={"displayModeBar": False},
figure={
"data": [
{
"x": data["Date"],
"y": data["Total Volume"],
"type": "lines",
},
],
"layout": {
"title": {
"text": "Avocados Sold",
"x": 0.05,
"xanchor": "left",
},
"xaxis": {"fixedrange": True},
"yaxis": {"fixedrange": True},
"colorway": ["#E12D39"],
},
},
),
className="card",
),
],
className="wrapper",
),
]
)
if __name__ == "__main__":
app.run_server(debug=True)
Код точно такой же, как и в app1.py, но с некоторыми изменениями. В строках с 21 по 37 вы можете увидеть, что в исходную версию панели мониторинга были внесены два изменения:
1. Появился новый элемент абзаца с эмодзи авокадо, который будет служить логотипом.
2. В каждом компоненте есть аргумент className. Эти имена классов должны соответствовать селектору классов в style.css, который будет определять внешний вид каждого компонента.
Например, класс header-description, назначенный компоненту абзаца, начинающемуся с «Analyze the behavior of avocado prices», имеет соответствующий селектор в style.css:
.header-description {
color: #CFCFCF;
margin: 4px auto;
text-align: center;
max-width: 384px;
}
Строки с 29 по 34 файла style.css определяют формат селектора класса описания заголовка. Они изменят цвет, поля, выравнивание и максимальную ширину любого компонента с className = "header-description". Все компоненты имеют соответствующие селекторы классов в файле CSS. Другое существенное изменение — графики. При построении графика цены, мы выполнили следующие изменения:
• Строка 43: мы удаляем плавающую полосу, которая отображается на графике по умолчанию.
• Строки 50 и 51: мы устанавливаем шаблон наведения таким образом, чтобы при наведении курсора на точку данных отображалась цена в долларах. Вместо 2,5он будет отображаться как 2,5 доллара.
• Строки с 54 по 66: мы настраиваем ось, цвет рисунка и формат заголовка в разделе макета графика.
• Строка 69: мы оборачиваем график в html.Div с классом «card». Это придаст графику белый фон и добавит небольшую тень под ним.
Аналогичные изменения внесены в графики продаж и объемов. После всех этих изменений наша панель управления должна выглядеть так:
Добавление интерактивности в приложения Dash с помощью обратных вызовов
В этом разделе мы будем добавлять интерактивные элементы на панель управления. Интерактивность Dash основана на парадигме реактивного программирования. Это означает, что вы можете связывать компоненты с элементами вашего приложения, которые хотите обновить. Если пользователь взаимодействует с компонентом ввода, например, с раскрывающимся списком или ползунком диапазона, то вывод, например, график, будет автоматически реагировать на изменения во вводе. Теперь давайте сделаем вашу панель управления интерактивной. Эта новая версия вашей панели инструментов позволит пользователю взаимодействовать со следующими фильтрами:
• Область
• Тип авокадо
• Диапазон дат
Поэтому, давайте создадим новый файл в папке assets с имеем style2.css и пропишем следующие команды:
body {
font-family: "Lato", sans-serif;
margin: 0;
background-color: #F7F7F7;
}
.header {
background-color: #222222;
height: 288px;
padding: 16px 0 0 0;
}
.header-emoji {
font-size: 48px;
margin: 0 auto;
text-align: center;
}
.header-title {
color: #FFFFFF;
font-size: 48px;
font-weight: bold;
text-align: center;
margin: 0 auto;
}
.header-description {
color: #CFCFCF;
margin: 4px auto;
text-align: center;
max-width: 384px;
}
.wrapper {
margin-right: auto;
margin-left: auto;
max-width: 1024px;
padding-right: 10px;
padding-left: 10px;
margin-top: 32px;
}
.card {
margin-bottom: 24px;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}
.menu {
height: 112px;
width: 912px;
display: flex;
justify-content: space-evenly;
padding-top: 24px;
margin: -80px auto 0 auto;
background-color: #FFFFFF;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}
.Select-control {
width: 256px;
height: 48px;
}
.Select--single > .Select-control .Select-value, .Select-placeholder {
line-height: 48px;
}
.Select--multi .Select-value-label {
line-height: 32px;
}
.menu-title {
margin-bottom: 6px;
font-weight: bold;
color: #079A82;
}
После этого, мы создаем еще один файл, но уже просто в папке с проектом, с именем app3.py, в котором прописываем следующий код:
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import numpy as np
from dash.dependencies import Output, Input
data = pd.read_csv("avocado.csv")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)
external_stylesheets = [
{
"href": "https://fonts.googleapis.com/css2?"
"family=Lato:wght@400;700&display=swap",
"rel": "stylesheet",
},
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Avocado Analytics: Understand Your Avocados!"
app.layout = html.Div(
children=[
html.Div(
children=[
html.P(children="🥑", className="header-emoji"),
html.H1(
children="Avocado Analytics", className="header-title"
),
html.P(
children="Analyze the behavior of avocado prices"
" and the number of avocados sold in the US"
" between 2015 and 2018",
className="header-description",
),
],
className="header",
),
html.Div(
children=[
html.Div(
children=[
html.Div(children="Region", className="menu-title"),
dcc.Dropdown(
id="region-filter",
options=[
{"label": region, "value": region}
for region in np.sort(data.region.unique())
],
value="Albany",
clearable=False,
className="dropdown",
),
]
),
html.Div(
children=[
html.Div(children="Type", className="menu-title"),
dcc.Dropdown(
id="type-filter",
options=[
{"label": avocado_type, "value": avocado_type}
for avocado_type in data.type.unique()
],
value="organic",
clearable=False,
searchable=False,
className="dropdown",
),
],
),
html.Div(
children=[
html.Div(
children="Date Range",
className="menu-title"
),
dcc.DatePickerRange(
id="date-range",
min_date_allowed=data.Date.min().date(),
max_date_allowed=data.Date.max().date(),
start_date=data.Date.min().date(),
end_date=data.Date.max().date(),
),
]
),
],
className="menu",
),
html.Div(
children=[
html.Div(
children=dcc.Graph(
id="price-chart", config={"displayModeBar": False},
),
className="card",
),
html.Div(
children=dcc.Graph(
id="volume-chart", config={"displayModeBar": False},
),
className="card",
),
],
className="wrapper",
),
]
)
@app.callback(
[Output("price-chart", "figure"), Output("volume-chart", "figure")],
[
Input("region-filter", "value"),
Input("type-filter", "value"),
Input("date-range", "start_date"),
Input("date-range", "end_date"),
],
)
def update_charts(region, avocado_type, start_date, end_date):
mask = (
(data.region == region)
& (data.type == avocado_type)
& (data.Date >= start_date)
& (data.Date <= end_date)
)
filtered_data = data.loc[mask, :]
price_chart_figure = {
"data": [
{
"x": filtered_data["Date"],
"y": filtered_data["AveragePrice"],
"type": "lines",
"hovertemplate": "$%{y:.2f}",
},
],
"layout": {
"title": {
"text": "Average Price of Avocados",
"x": 0.05,
"xanchor": "left",
},
"xaxis": {"fixedrange": True},
"yaxis": {"tickprefix": "$", "fixedrange": True},
"colorway": ["#17B897"],
},
}
volume_chart_figure = {
"data": [
{
"x": filtered_data["Date"],
"y": filtered_data["Total Volume"],
"type": "lines",
},
],
"layout": {
"title": {"text": "Avocados Sold", "x": 0.05, "xanchor": "left"},
"xaxis": {"fixedrange": True},
"yaxis": {"fixedrange": True},
"colorway": ["#E12D39"],
},
}
return price_chart_figure, volume_chart_figure
if __name__ == "__main__":
app.run_server(debug=True)
Данный код очень схож с app1 и app2, но он так же претерпел изменения. В строках с 24 по 74 мы определяем html.Div поверх наших графиков, состоящих из двух раскрывающихся списков и селектора диапазона дат. Он будет служить меню, которое пользователь будет использовать для взаимодействия с данными:
В строках с 41 по 55 мы определяем раскрывающийся список, который пользователи будут использовать для фильтрации данных по региону. Помимо заголовка, в нем есть компонент dcc.Dropdown. Вот что означает каждый из параметров:
• id — идентификатор элемента.
• options — это параметры, отображаемые при выборе раскрывающегося списка. Он ожидает словарь с метками и значениями.
• value — значение по умолчанию при загрузке страницы.
• clearable позволяет пользователю оставить это поле пустым, если установлено значение True.
• className — это селектор классов, используемый для применения стилей.
Селекторы «Type» и «Date Range» имеют ту же структуру, что и раскрывающееся меню «Region».
В строках с 90 по 106 мы определяем компоненты dcc.Graph. Возможно, вы заметили, что по сравнению с предыдущей версией панели инструментов в компонентах отсутствует аргумент figure. Это связано с тем, что аргумент figure теперь будет генерироваться функцией обратного вызова с использованием входных данных, которые пользователь устанавливает с помощью селекторов «Region», «Type» и «Date Range».
Но пока что мы только определили, как пользователь будет взаимодействовать с нашим приложением. Теперь нам нужно заставить ваше приложение реагировать на действия пользователя. Для этого вы воспользуетесь функциями обратного вызова. Функции обратного вызова Dash — это обычные функции Python с декоратором app.callback. В Dash при изменении ввода запускается функция обратного вызова. Функция выполняет некоторые заранее определенные операции, такие как фильтрация набора данных, и возвращает результат в приложение. По сути, обратные вызовы связывают входы и выходы в вашем приложении. Сама функция обратного вызова, используемая для обновления графиков, представлена со строки 111 по 164.
В строках 111–119 мы определяем входы и выходы внутри декоратора app.callback. Сначала мы определяем выходы с помощью объектов вывода. Эти объекты принимают два аргумента:
1. Идентификатор элемента, который они изменят при выполнении функции.
2. Свойство изменяемого элемента.
Например, Output("price-chart", "figure") обновит свойство figure элемента «price-chart». Затем мы определяем входы с помощью объектов Input. Они также принимают два аргумента:
1. Идентификатор элемента, за изменениями которого они будут следить.
2. Свойство наблюдаемого элемента, которое они должны принимать, когда происходит изменение.
Итак, Input("region-filter", "value") будет следить за элементом «region-filter» на предмет изменений и примет его свойство value, если элемент изменится.
В строке 120 мы определяем функцию, которая будет применяться при изменении ввода. Здесь следует отметить, что аргументы функции будут соответствовать порядку объектов Input, переданных в обратный вызов. Нет явной связи между именами аргументов в функции и значениями, указанными во входных объектах.
Наконец, в строках 121–164 вы определяете тело функции. В этом случае функция принимает входные данные (region, type of avocado и date range), фильтрует данные и генерирует объекты-фигуры для графиков цен и объемов.
Исходя из всего проделанного выше, мы можем использовать Dash для создания аналитических приложений, что на данный момент очень востребовано и ценно в современном мире.
Разработка интерфейсов визуализации данных на Python с помощью Dash
Прежде чем приступить к выполнению работы, давайте разберем, что же такое Dash? Dash — это платформа с открытым исходным кодом для создания интерфейсов визуализации данных, помогающая специалистам по обработке данных создавать аналитические веб-приложения, не требуя передовых знаний в области веб-разработки.
Я всегда работаю в PyCharm, поэтому, прежде чем приступить к работе, я создаю проект, в котором создается виртуальная среда. Далее, я добавляю в проект файл, с которым будет происходить работа, а именно: avocado.csv, содержащий набор данных о продажах и ценах на авокадо в США за период с 2015 по 2018 годы. Скачать данный файл можно здесь. После этого, необходимо установить следующие библиотеки: dash и pandas. Теперь мы можем приступать к работе!
Создание первого приложения
Давайте создадим пустой файл с именем app1.py в корневом каталоге нашего проекта и создадим наше первое приложение:
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
data = pd.read_csv("avocado.csv")
data = data.query("type == 'conventional' and region == 'Albany'")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)
app = dash.Dash(__name__)
app.layout = html.Div(
children=[
html.H1(
children="Avocado Analytics",
style={"fontSize": "48px", "color": "red"},
),
html.P(
children="Analyze the behavior of avocado prices"
" and the number of avocados sold in the US"
" between 2015 and 2018",
),
dcc.Graph(
figure={
"data": [
{
"x": data["Date"],
"y": data["AveragePrice"],
"type": "lines",
},
],
"layout": {"title": "Average Price of Avocados"},
},
),
dcc.Graph(
figure={
"data": [
{
"x": data["Date"],
"y": data["Total Volume"],
"type": "lines",
},
],
"layout": {"title": "Avocados Sold"},
},
),
]
)
if __name__ == "__main__":
app.run_server(debug=True)
В строках с 1 по 4 мы импортируем необходимые библиотеки: dash, dash_core_components, dash_html_components и pandas. Каждая библиотека предоставляет стандартный блок для вашего приложения:
• dash поможет нам инициализировать ваше приложение.
• dash_core_components позволяет создавать интерактивные компоненты, такие как графики, раскрывающиеся списки или диапазоны дат.
• dash_html_components позволяет получить доступ к тегам HTML.
• pandas помогает читать и систематизировать данные.
В строках с 6 по 9 мы считываем данные и предварительно обрабатываем их для использования на панели управления. Мы фильтруем некоторые данные, потому что текущая версия нашей информационной панели не является интерактивной и в противном случае значения на графике не имели бы смысла. В строке 11, мы создаем экземпляр класса Dash. Если вы раньше использовали Flask, то инициализация класса Dash может показаться вам знакомой. В Flask вы обычно инициализируете приложение WSGI с помощью Flask(__name__). Точно так же для приложения Dash вы используете Dash(__name__).
Далее, мы определяем свойство макета нашего приложения, которое определяет внешний вид нашего приложения. В нашем случае мы будем использовать заголовок с описанием под ним и двумя графиками В строках с 13 по 23 вы можете увидеть на практике компоненты Dash HTML. Мы начинаем с определения родительского компонента, html.Div. Затем мы добавляем еще два элемента, заголовок (html.H1) и абзац (html.P) в качестве его дочерних элементов. Эти компоненты эквивалентны div, HTML-теги h1 и p. Вы можете использовать аргументы компонентов для изменения атрибутов или содержимого тегов. Например, чтобы указать, что находится внутри тега div, мы используем аргумент children в html.Div. В компонентах также есть другие аргументы, такие как style, className или id, которые относятся к атрибутам тегов HTML. При этом, если вы хотите изменить размер и цвет элемента H1, то достаточно прописать изменения через style, как это сделал я в 17 строке.
В строках 24–50 фрагмента кода макета вы можете увидеть компонент графика из Dash Core Components на практике. В app.layout есть два компонента dcc.Graph. Первый отображает средние цены на авокадо за период исследования, а второй — количество авокадо, проданных в США за тот же период. Под капотом Dash использует Plotly.js для создания графиков. Компоненты dcc.Graph ожидают объект фигуры или словарь Python, содержащий данные графика и макет. В этом случае вы предоставляете последнее.
И последние две строчки кода позволяют запускать приложение Dash локально, используя встроенный сервер Flask. Далее, мы запускаем наше приложение, после чего переходим по высветившейся ссылке и смотрим результат:
Создание второго приложения с добавлением внешних ресурсов
В данной главе вы узнаете, как настроить внешний вид панели инструментов, а именно:
• Добавление значка и заголовка на страницу.
• Изменение семейства шрифтов вашей панели инструментов.
• Использование внешних файлов CSS для стилизации компонентов Dash.
Сначала, в папке с проектом, мы создаем еще одну папку с именем assets, в которую добавляем скаченный значок с именем favicon.ico (скачать его можно здесь) и создаем файл style.css, содержащий следующие команды:
body {
font-family: "Lato", sans-serif;
margin: 0;
background-color: #F7F7F7;
}
.header {
background-color: #222222;
height: 256px;
display: flex;
flex-direction: column;
justify-content: center;
}
.header-emoji {
font-size: 48px;
margin: 0 auto;
text-align: center;
}
.header-title {
color: #FFFFFF;
font-size: 48px;
font-weight: bold;
text-align: center;
margin: 0 auto;
}
.header-description {
color: #CFCFCF;
margin: 4px auto;
text-align: center;
max-width: 384px;
}
.wrapper {
margin-right: auto;
margin-left: auto;
max-width: 1024px;
padding-right: 10px;
padding-left: 10px;
margin-top: 32px;
}
.card {
margin-bottom: 24px;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}
Файлы в папке assets содержат стили, которые мы применим к компонентам в макете приложения. После запуска сервера, Dash автоматически обслуживает файлы, расположенные в этой папке. И для установки значка по умолчанию нам не нужно предпринимать никаких дополнительных действий. Для использования стилей, определенных в style.css, нам необходимо использовать аргумент className в компонентах Dash. Поэтому, давайте создадим новый файл app2.py, в котором пропишем следующий код:
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
data = pd.read_csv("avocado.csv")
data = data.query("type == 'conventional' and region == 'Albany'")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)
external_stylesheets = [
{
"href": "https://fonts.googleapis.com/css2?"
"family=Lato:wght@400;700&display=swap",
"rel": "stylesheet",
},
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Avocado Analytics: Understand Your Avocados!"
app.layout = html.Div(
children=[
html.Div(
children=[
html.P(children="🥑", className="header-emoji"),
html.H1(
children="Avocado Analytics", className="header-title"
),
html.P(
children="Analyze the behavior of avocado prices"
" and the number of avocados sold in the US"
" between 2015 and 2018",
className="header-description",
),
],
className="header",
),
html.Div(
children=[
html.Div(
children=dcc.Graph(
id="price-chart",
config={"displayModeBar": False},
figure={
"data": [
{
"x": data["Date"],
"y": data["AveragePrice"],
"type": "lines",
"hovertemplate": "$%{y:.2f}"
"",
},
],
"layout": {
"title": {
"text": "Average Price of Avocados",
"x": 0.05,
"xanchor": "left",
},
"xaxis": {"fixedrange": True},
"yaxis": {
"tickprefix": "$",
"fixedrange": True,
},
"colorway": ["#17B897"],
},
},
),
className="card",
),
html.Div(
children=dcc.Graph(
id="volume-chart",
config={"displayModeBar": False},
figure={
"data": [
{
"x": data["Date"],
"y": data["Total Volume"],
"type": "lines",
},
],
"layout": {
"title": {
"text": "Avocados Sold",
"x": 0.05,
"xanchor": "left",
},
"xaxis": {"fixedrange": True},
"yaxis": {"fixedrange": True},
"colorway": ["#E12D39"],
},
},
),
className="card",
),
],
className="wrapper",
),
]
)
if __name__ == "__main__":
app.run_server(debug=True)
Код точно такой же, как и в app1.py, но с некоторыми изменениями. В строках с 21 по 37 вы можете увидеть, что в исходную версию панели мониторинга были внесены два изменения:
1. Появился новый элемент абзаца с эмодзи авокадо, который будет служить логотипом.
2. В каждом компоненте есть аргумент className. Эти имена классов должны соответствовать селектору классов в style.css, который будет определять внешний вид каждого компонента.
Например, класс header-description, назначенный компоненту абзаца, начинающемуся с «Analyze the behavior of avocado prices», имеет соответствующий селектор в style.css:
.header-description {
color: #CFCFCF;
margin: 4px auto;
text-align: center;
max-width: 384px;
}
Строки с 29 по 34 файла style.css определяют формат селектора класса описания заголовка. Они изменят цвет, поля, выравнивание и максимальную ширину любого компонента с className = "header-description". Все компоненты имеют соответствующие селекторы классов в файле CSS. Другое существенное изменение — графики. При построении графика цены, мы выполнили следующие изменения:
• Строка 43: мы удаляем плавающую полосу, которая отображается на графике по умолчанию.
• Строки 50 и 51: мы устанавливаем шаблон наведения таким образом, чтобы при наведении курсора на точку данных отображалась цена в долларах. Вместо 2,5он будет отображаться как 2,5 доллара.
• Строки с 54 по 66: мы настраиваем ось, цвет рисунка и формат заголовка в разделе макета графика.
• Строка 69: мы оборачиваем график в html.Div с классом «card». Это придаст графику белый фон и добавит небольшую тень под ним.
Аналогичные изменения внесены в графики продаж и объемов. После всех этих изменений наша панель управления должна выглядеть так:
Добавление интерактивности в приложения Dash с помощью обратных вызовов
В этом разделе мы будем добавлять интерактивные элементы на панель управления. Интерактивность Dash основана на парадигме реактивного программирования. Это означает, что вы можете связывать компоненты с элементами вашего приложения, которые хотите обновить. Если пользователь взаимодействует с компонентом ввода, например, с раскрывающимся списком или ползунком диапазона, то вывод, например, график, будет автоматически реагировать на изменения во вводе. Теперь давайте сделаем вашу панель управления интерактивной. Эта новая версия вашей панели инструментов позволит пользователю взаимодействовать со следующими фильтрами:
• Область
• Тип авокадо
• Диапазон дат
Поэтому, давайте создадим новый файл в папке assets с имеем style2.css и пропишем следующие команды:
body {
font-family: "Lato", sans-serif;
margin: 0;
background-color: #F7F7F7;
}
.header {
background-color: #222222;
height: 288px;
padding: 16px 0 0 0;
}
.header-emoji {
font-size: 48px;
margin: 0 auto;
text-align: center;
}
.header-title {
color: #FFFFFF;
font-size: 48px;
font-weight: bold;
text-align: center;
margin: 0 auto;
}
.header-description {
color: #CFCFCF;
margin: 4px auto;
text-align: center;
max-width: 384px;
}
.wrapper {
margin-right: auto;
margin-left: auto;
max-width: 1024px;
padding-right: 10px;
padding-left: 10px;
margin-top: 32px;
}
.card {
margin-bottom: 24px;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}
.menu {
height: 112px;
width: 912px;
display: flex;
justify-content: space-evenly;
padding-top: 24px;
margin: -80px auto 0 auto;
background-color: #FFFFFF;
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.18);
}
.Select-control {
width: 256px;
height: 48px;
}
.Select--single > .Select-control .Select-value, .Select-placeholder {
line-height: 48px;
}
.Select--multi .Select-value-label {
line-height: 32px;
}
.menu-title {
margin-bottom: 6px;
font-weight: bold;
color: #079A82;
}
После этого, мы создаем еще один файл, но уже просто в папке с проектом, с именем app3.py, в котором прописываем следующий код:
import dash
import dash_core_components as dcc
import dash_html_components as html
import pandas as pd
import numpy as np
from dash.dependencies import Output, Input
data = pd.read_csv("avocado.csv")
data["Date"] = pd.to_datetime(data["Date"], format="%Y-%m-%d")
data.sort_values("Date", inplace=True)
external_stylesheets = [
{
"href": "https://fonts.googleapis.com/css2?"
"family=Lato:wght@400;700&display=swap",
"rel": "stylesheet",
},
]
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app.title = "Avocado Analytics: Understand Your Avocados!"
app.layout = html.Div(
children=[
html.Div(
children=[
html.P(children="🥑", className="header-emoji"),
html.H1(
children="Avocado Analytics", className="header-title"
),
html.P(
children="Analyze the behavior of avocado prices"
" and the number of avocados sold in the US"
" between 2015 and 2018",
className="header-description",
),
],
className="header",
),
html.Div(
children=[
html.Div(
children=[
html.Div(children="Region", className="menu-title"),
dcc.Dropdown(
id="region-filter",
options=[
{"label": region, "value": region}
for region in np.sort(data.region.unique())
],
value="Albany",
clearable=False,
className="dropdown",
),
]
),
html.Div(
children=[
html.Div(children="Type", className="menu-title"),
dcc.Dropdown(
id="type-filter",
options=[
{"label": avocado_type, "value": avocado_type}
for avocado_type in data.type.unique()
],
value="organic",
clearable=False,
searchable=False,
className="dropdown",
),
],
),
html.Div(
children=[
html.Div(
children="Date Range",
className="menu-title"
),
dcc.DatePickerRange(
id="date-range",
min_date_allowed=data.Date.min().date(),
max_date_allowed=data.Date.max().date(),
start_date=data.Date.min().date(),
end_date=data.Date.max().date(),
),
]
),
],
className="menu",
),
html.Div(
children=[
html.Div(
children=dcc.Graph(
id="price-chart", config={"displayModeBar": False},
),
className="card",
),
html.Div(
children=dcc.Graph(
id="volume-chart", config={"displayModeBar": False},
),
className="card",
),
],
className="wrapper",
),
]
)
@app.callback(
[Output("price-chart", "figure"), Output("volume-chart", "figure")],
[
Input("region-filter", "value"),
Input("type-filter", "value"),
Input("date-range", "start_date"),
Input("date-range", "end_date"),
],
)
def update_charts(region, avocado_type, start_date, end_date):
mask = (
(data.region == region)
& (data.type == avocado_type)
& (data.Date >= start_date)
& (data.Date <= end_date)
)
filtered_data = data.loc[mask, :]
price_chart_figure = {
"data": [
{
"x": filtered_data["Date"],
"y": filtered_data["AveragePrice"],
"type": "lines",
"hovertemplate": "$%{y:.2f}",
},
],
"layout": {
"title": {
"text": "Average Price of Avocados",
"x": 0.05,
"xanchor": "left",
},
"xaxis": {"fixedrange": True},
"yaxis": {"tickprefix": "$", "fixedrange": True},
"colorway": ["#17B897"],
},
}
volume_chart_figure = {
"data": [
{
"x": filtered_data["Date"],
"y": filtered_data["Total Volume"],
"type": "lines",
},
],
"layout": {
"title": {"text": "Avocados Sold", "x": 0.05, "xanchor": "left"},
"xaxis": {"fixedrange": True},
"yaxis": {"fixedrange": True},
"colorway": ["#E12D39"],
},
}
return price_chart_figure, volume_chart_figure
if __name__ == "__main__":
app.run_server(debug=True)
Данный код очень схож с app1 и app2, но он так же претерпел изменения. В строках с 24 по 74 мы определяем html.Div поверх наших графиков, состоящих из двух раскрывающихся списков и селектора диапазона дат. Он будет служить меню, которое пользователь будет использовать для взаимодействия с данными:
В строках с 41 по 55 мы определяем раскрывающийся список, который пользователи будут использовать для фильтрации данных по региону. Помимо заголовка, в нем есть компонент dcc.Dropdown. Вот что означает каждый из параметров:
• id — идентификатор элемента.
• options — это параметры, отображаемые при выборе раскрывающегося списка. Он ожидает словарь с метками и значениями.
• value — значение по умолчанию при загрузке страницы.
• clearable позволяет пользователю оставить это поле пустым, если установлено значение True.
• className — это селектор классов, используемый для применения стилей.
Селекторы «Type» и «Date Range» имеют ту же структуру, что и раскрывающееся меню «Region».
В строках с 90 по 106 мы определяем компоненты dcc.Graph. Возможно, вы заметили, что по сравнению с предыдущей версией панели инструментов в компонентах отсутствует аргумент figure. Это связано с тем, что аргумент figure теперь будет генерироваться функцией обратного вызова с использованием входных данных, которые пользователь устанавливает с помощью селекторов «Region», «Type» и «Date Range».
Но пока что мы только определили, как пользователь будет взаимодействовать с нашим приложением. Теперь нам нужно заставить ваше приложение реагировать на действия пользователя. Для этого вы воспользуетесь функциями обратного вызова. Функции обратного вызова Dash — это обычные функции Python с декоратором app.callback. В Dash при изменении ввода запускается функция обратного вызова. Функция выполняет некоторые заранее определенные операции, такие как фильтрация набора данных, и возвращает результат в приложение. По сути, обратные вызовы связывают входы и выходы в вашем приложении. Сама функция обратного вызова, используемая для обновления графиков, представлена со строки 111 по 164.
В строках 111–119 мы определяем входы и выходы внутри декоратора app.callback. Сначала мы определяем выходы с помощью объектов вывода. Эти объекты принимают два аргумента:
1. Идентификатор элемента, который они изменят при выполнении функции.
2. Свойство изменяемого элемента.
Например, Output("price-chart", "figure") обновит свойство figure элемента «price-chart». Затем мы определяем входы с помощью объектов Input. Они также принимают два аргумента:
1. Идентификатор элемента, за изменениями которого они будут следить.
2. Свойство наблюдаемого элемента, которое они должны принимать, когда происходит изменение.
Итак, Input("region-filter", "value") будет следить за элементом «region-filter» на предмет изменений и примет его свойство value, если элемент изменится.
В строке 120 мы определяем функцию, которая будет применяться при изменении ввода. Здесь следует отметить, что аргументы функции будут соответствовать порядку объектов Input, переданных в обратный вызов. Нет явной связи между именами аргументов в функции и значениями, указанными во входных объектах.
Наконец, в строках 121–164 вы определяете тело функции. В этом случае функция принимает входные данные (region, type of avocado и date range), фильтрует данные и генерирует объекты-фигуры для графиков цен и объемов.
Исходя из всего проделанного выше, мы можем использовать Dash для создания аналитических приложений, что на данный момент очень востребовано и ценно в современном мире.
Очистка данных с помощью 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.
Работа с PDF-файлами
Сначала сделаем извлечение текста двумя методами. Первый – используя библиотеку 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))
В пакете 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)
Автоматизация индивидуальной нарезки файлов Excel и рассылка их на eMail
Автоматизация индивидуальной нарезки файлов 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)
Благодаря данной программе, мы смогли автоматизировать достаточно болезненную ручную работу и сэкономить просто огромное количество своего времени и сил.