Share This
Связаться со мной
Крути в низ
Categories
//Django, Pandas и Chart.js для быстрой панели инструментов

Django, Pandas и Chart.js для быстрой панели инструментов

Попробуем разобраться, как можно использовать Django, Pandas и Chart.js для быстрого отображения данных в виде различных графиков и диаграмм. Обсудить

django pandas i chartjs dlja bystroj paneli instrumentov d69f1c2 - Django, Pandas и Chart.js для быстрой панели инструментов

Перевод публикуется с сокращениями, автор оригинальной статьи Shane Gary.

django pandas i chartjs dlja bystroj paneli instrumentov 1009584 - Django, Pandas и Chart.js для быстрой панели инструментов

Эту связку полезно применять для быстрой визуализации одной таблицы с несколькими различными графиками, а также для создания надежного сайта на Django и Chart.js.

Почему Pandas? Все задачи можно выполнить изнутри Django непосредственно из БД, т. к. правильные запросы к базе всегда будут выгоднее для продакшена.

Почему Django? Это отличный выбор для проекта, где необходимо экспортировать данные в одну таблицу. Использование Flask будет более выигрышным вариантом, если не нужна аутентификация и доступ к данным из существующей базы.

Почему Chart.js? Если вы хотите развернуть кучу различных интерактивных диаграмм, изменив несколько переменных – Chart.js сделает это быстрее других.

Исходные тексты и простой набор данных из туториала вы найдете по ссылкам.

Настройка Django

Все действия в этом проекте будут происходить на сервере с Debian.

         pip install django pandas django-admin startproject django_charts cd django_charts python manage.py migrate python manage.py createsuperuser python manage.py startapp data cd data  mkdir templates cd .. python manage.py runserver     

Дополнительно можете установить себе palettable – цветовую палитру для Python, однако код будет работать и без нее.

         pip install palettable     

На основе набора данных мы создадим следующую модель, но она должна быть изменена под ваши нужды.

         from django.db import models  class Purchase(models.Model):     city = models.CharField(max_length=50)     customer_type = models.CharField(max_length=50)     gender = models.CharField(max_length=50)     unit_price = models.FloatField()     quantity = models.IntegerField()     product_line = models.CharField(max_length=50)     tax = models.FloatField()     total = models.FloatField()     date = models.DateField()     time = models.TimeField()     payment	= models.CharField(max_length=50)     cogs = models.FloatField()     profit = models.FloatField()     rating  = models.FloatField()     

Обязательно обновите БД после ее создания:

         python manage.py makemigrations python manage.py migrate     

С помощью Pandas и Django сделайте загрузку csv в базу данных для Kaggle:

         import pandas as pd from .models import Purchase  # dataset from https://www.kaggle.com/aungpyaeap/supermarket-sales # headers changed and invoice number col removed def csv_to_db():     df = pd.read_csv('supermarket_sales.csv') # use pandas to read the csv     records = df.to_records()  # convert to records      # loop through and create a purchase object using django     for record in records:         purchase = Purchase(             city=record[3],             customer_type=record[4],             gender=record[5],             product_line=record[6],             unit_price=record[7],             quantity=record[8],             tax=record[9],             total=record[10],             date=datetime.strptime(record[11], '%m/%d/%Y').date(),             time=record[12],             payment=record[13],             cogs=record[14],             profit=record[16],             rating=record[17],         )         purchase.save()      

Импортируйте себе всю эту штуку:

         from django.views.generic import TemplateView from .methods import csv_to_db class Dashboard(TemplateView):     template_name = 'dashboard.html'     def get_context_data(self, **kwargs):          # get the data from the default method                context = super().get_context_data(**kwargs)         csv_to_db()     

Затем создайте пустой файл base/dashboard.html и data/urls.py:

         from django.urls import path from data import views  urlpatterns = [     path('', views.Dashboard.as_view(), name='dashboard') ]     

Отредактируйте django_charts/urls.py, чтобы добавить URL для данных:

         from django.contrib import admin from django.urls import path, include  urlpatterns = [     path('admin/', admin.site.urls),     path('data/', include('data.urls')), ]     

Обновите список django_charts/settings.py, чтобы включить data в INSTALLED_APPS:

         INSTALLED_APPS = [     'django.contrib.admin',     'django.contrib.auth',     'django.contrib.contenttypes',     'django.contrib.sessions',     'django.contrib.messages',     'django.contrib.staticfiles',     'data', ]     

Убедимся, что можем видеть покупку в админке, добавив это в data/admin.py:

         from django.contrib import admin from .models import Purchase  admin.site.register(Purchase)     

Теперь вы можете перейти на страницу дашборда (127.0.0.1:8000/data/), а потом проверить в админке (127.0.0.1:8000/admin/data/purchase/), видны ли все записи. Если вы используете тот же набор данных, их должно быть около 1000.

Настройка HTML

Есть базовый файл, который мы будем расширять:

         {% load static %}  <!doctype html> <html lang="en">   <head>     <title>       {% block title %}{% endblock %}     </title>          <meta charset="utf-8">     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">      <link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.png' %}">      <!-- Bootstrap CSS -->     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous">    </head>   <body>     {% block page_content %}{% endblock %}     <!-- jQuery first, then Popper.js, then Bootstrap JS -->     <script         src="https://code.jquery.com/jquery-3.5.1.min.js"         integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="         crossorigin="anonymous">     </script>     <script          src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"          integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"          crossorigin="anonymous">     </script>     <script          src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"          integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI"          crossorigin="anonymous">     </script>      <!-- Chart.JS -->     <script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3"></script>      <script>       $(document).ready(function(){         {% block js_scripts %}{% endblock %}       })     </script>    </body> </html>     

Немного модернизируем его:

         {% block title %}{% endblock %} {% block custom_css %}{% endblock %} {% block page_content %}{% endblock %} {% block js_scripts %}{% endblock %}     

Это даст возможность добавлять блоки с любой страницы. Обратите внимание, что здесь включен bootstrap CDN, чтобы использовать popper.js (это опционально). Можно выполнить проект совсем без bootstrap, но придется отредактировать dashboard.html.

Все написанные сценарии обернуты в $(document).ready, чтобы не происходило никаких манипуляций до тех пор, пока страница не будет готова.

Далее рассмотрим страницу с графиками – dashboard.html:

         {% extends 'base.html' %}  {% block page_content %}     <div class="container">         <div class="row mb-4 mt-4">             <div class="col"                 <div class="card-deck">                     {% for chart in charts %}                         <div class="card">                             <div class="card-body">                                 <div class="chart-container" style="height:150; width:150">                                     {{ chart.html|safe }}                                 </div>                             </div>                         </div>                         {% if forloop.counter|divisibleby:2 %}                             <div class="w-100 d-none d-sm-block d-md-none mb-4"><!-- wrap every 2 on sm--></div>                         {% endif %}                         {% if forloop.counter|divisibleby:3 %}                             <div class="w-100 d-none d-md-block d-lg-none mb-4"><!-- wrap every 3 on md--></div>                         {% endif %}                         {% if forloop.counter|divisibleby:4 %}                             <div class="w-100 d-none d-lg-block d-xl-none mb-4"><!-- wrap every 4 on lg--></div>                         {% endif %}                         {% if forloop.counter|divisibleby:5 %}                             <div class="w-100 d-none d-xl-block mb-4"><!-- wrap every 5 on xl--></div>                         {% endif %}                     {% endfor %}                 </div>             </div>         </div>     </div> {% endblock %}  {% block js_scripts %}     {% for chart in charts %}         {{ chart.js|safe }}     {% endfor %} {% endblock %}     

Здесь создается контейнер, строка и колонка, внутри которой лежит «колода карт», создающая карты одинакового размера.

Колода карт будет пытаться втиснуть все в одну строку. Счетчики forloop.counters используются для определения размера экрана и соответствующего обертывания колоды карт, однако на практике вы обнаружите, что Chart.js зачастую игнорирует настройки размера холста.

views.py

Чтобы лучше понять происходящее, посмотрим на views.py:

         import pandas as pd import numpy as np from django.views.generic import TemplateView from .methods import csv_to_db from .models import Purchase from .charts import objects_to_df, Chart  PALETTE = ['#465b65', '#184c9c', '#d33035', '#ffc107', '#28a745', '#6f7f8c', '#6610f2', '#6e9fa5', '#fd7e14', '#e83e8c', '#17a2b8', '#6f42c1' ]  class Dashboard(TemplateView):     template_name = 'dashboard.html'      def get_context_data(self, **kwargs):          # получение данные из метода по умолчанию         context = super().get_context_data(**kwargs)          # поля, которые мы будем использовать         # df_fields = ['city', 'customer_type', 'gender', 'unit_price', 'quantity',          #     'product_line', 'tax', 'total' , 'date', 'time', 'payment',          #     'cogs', 'profit', 'rating']          # поля для исключения         # df_exclude = ['id', 'cogs']                  # создание фрейма данных с записями. chart.js не очень хорошо справляется          # с датами во всех ситуациях, поэтому наш метод преобразует их в строки         # и нужно будет определить столбцы дат и нужный формат.                  df = objects_to_df(Purchase, date_cols=['%Y-%m', 'date'])          # создание контекста charts для хранения всех графиков         context['charts'] = []          ### каждая диаграмма добавляется одинаково поэтому опишем первую         # создадим объект диаграммы с уникальным chart_id и цветовой палитрой         # если не указан chart_id или цветовая палитра, они будут генерироваться рандомно         # тип диаграмм должен быть идентифицирован здесь и отличаться от типа chartjs         city_payment_radar = Chart('radar', chart_id='city_payment_radar', palette=PALETTE)         # создадим сводную таблицу pandas на основе полей и агрегации, которые мы хотим         # стеки используются либо для группировки, либо для укладки определенного столбца         city_payment_radar.from_df(df, values='total', stacks=['payment'], labels=['city'])         # добавим контекст         context['charts'].append(city_payment_radar.get_presentation())          exp_polar = Chart('polarArea', chart_id='polar01', palette=PALETTE)         exp_polar.from_df(df, values='total', labels=['payment'])         context['charts'].append(exp_polar.get_presentation())          exp_doughnut = Chart('doughnut', chart_id='doughnut01', palette=PALETTE)         exp_doughnut.from_df(df, values='total', labels=['city'])         context['charts'].append(exp_doughnut.get_presentation())          exp_bar = Chart('bar', chart_id='bar01', palette=PALETTE)         exp_bar.from_df(df, values='total', labels=['city'])         context['charts'].append(exp_bar.get_presentation())          city_payment = Chart('groupedBar', chart_id='city_payment', palette=PALETTE)         city_payment.from_df(df, values='total', stacks=['payment'], labels=['date'])         context['charts'].append(city_payment.get_presentation())          city_payment_h = Chart('horizontalBar', chart_id='city_payment_h', palette=PALETTE)         city_payment_h.from_df(df, values='total', stacks=['payment'], labels=['city'])         context['charts'].append(city_payment_h.get_presentation())          city_gender_h = Chart('stackedHorizontalBar', chart_id='city_gender_h', palette=PALETTE)         city_gender_h.from_df(df, values='total', stacks=['gender'], labels=['city'])         context['charts'].append(city_gender_h.get_presentation())          city_gender = Chart('stackedBar', chart_id='city_gender', palette=PALETTE)         city_gender.from_df(df, values='total', stacks=['gender'], labels=['city'])         context['charts'].append(city_gender.get_presentation())          return context     

Будем использовать TemplateView. Это очень простое view, к которому можно что-то добавить. Единственный метод, который необходимо расширить – get_context_data, использующийся в Django для получения данных.

Мы вытаскиваем нужные объекты и создаем фрейм данных. Известно, что Chart.js не очень хорошо работает с датами – конвертируем их в строки после создания фрейма. Затем добавляем каждый график в контекст Chart. Это позволяет перебирать графики в коде HTML, т. к. каждая диаграмма представляет собой словарь, содержащий гипертекст и js-запись.

Charts.py

И наконец файл data/charts.py. Весь код легко переносится в другой проект, и вы можете просто поместить его в свой view. Пробежимся по некоторым функциям, а затем перейдем к классу Chart.

         def objects_to_df(model, fields=None, exclude=None, date_cols=None, **kwargs):     """     Возвращает фрейм данных pandas, содержащий записи в модели     ``fields`` это необязательный список имен полей. Если это предусмотрено, вернется только имя.     ``exclude`` это необязательный список имен полей. Если это предусмотрено,  именованные элементы исключатся из возвращаемого dict     ``date_cols`` chart.js в настоящее время он не очень хорошо обрабатывает даты, поэтому эти столбцы должны быть преобразованы в строку.     ``kwargs`` можно включить, чтобы ограничить модельный запрос конкретными записями     """          if not fields:         fields = [field.name for field in model._meta.get_fields()]      if exclude:         fields = [field for field in fields if field not in exclude]      records = model.objects.filter(**kwargs).values_list(*fields)     df = pd.DataFrame(list(records), columns=fields)      if date_cols:         strftime = date_cols.pop(0)         for date_col in date_cols:             df[date_col] = df[date_col].apply(lambda x: x.strftime(strftime))          return df     

Эта функция принимает модель Django и возвращает все записи. Можно включить некоторые фильтры после всех аргументов имени. Например:

         objects_to_df(Purchase, fields=None, exclude=['id'], date_cols=['%Y-%m', 'date'], city='Mandalay')      

Результат будет ограничен городом Мандалай. Поля include и exclude работают аналогично Django. Если вы ничего не включаете, будут выведены все поля из модели. Важно отметить, что exclude обрабатываются после include. Таким образом, если включить и исключить столбец, он не будет отображаться.

         def get_options():     """     Дефолтное значение для всех графиков     """     return {}     

Этот код устанавливает параметры по умолчанию для всех графиков.

         def generate_chart_id():     """     Вернет 8 рандомных сгенерированных символов ascii      """     return ''.join(random.choice(string.ascii_letters) for i in range(8))     

Если вы не хотите утруждать себя маркировкой каждого из них, данная функция устанавливает случайный ID, используемый JS для изменения элемента HTML. Этот идентификатор также используется для именования функции JS и переменной, чтобы облегчить отладку.

         from palettable.lightbartlein.diverging import BlueDarkRed12_6 as palette # example import  def get_colors():     """     Цвета из palette.colors или случайным образом сгенерированный список цветов.     Отлично работает с модулем palettable     но не является обязательным и будет вызывать get_random_colors     если palette.colors не задана     """     try:         return palette.hex_colors     except:         return get_random_colors(6)     

get_colors применяется для установки начальных цветов. Если ее передать в палитру, она будет использоваться в качестве основной. Если ничего не передавать и не импортировать из palletable, то цвета будут генерироваться случайным образом.

         def get_random_colors(num, colors=[]):          while len(colors) < num:         color = "#{:06x}".format(random.randint(0, 0xFFFFFF))          if color not in colors:             colors.append(color)      return colors     

get_random_colors используется, если цвета не были переданы или когда их больше, чем значений. Все начинается с переданных цветов, а в процессе они добавляются случайным образом.

По функциям все. Ниже приведен класс Chart.

         @dataclass class Chart:     """     Класс в помощь к chart.js.     ``datasets`` собственно данные. Содержит данные и варианты их получения.     ``labels`` метки для данных.     ``chart_id`` уникальный ID диаграммы. Будет сгенерирован рандомно     если таковой не предусмотрен. Это должна быть допустимая JS-переменная.     Не используйте '-'     ``palette`` список цветов.  Будет сгенерирован, если ни один не указан.     """     chart_type: str     datasets: List = field(default_factory=list)     labels: List = field(default_factory=list)     chart_id: str = field(default_factory=generate_chart_id)     palette: List = field(default_factory=get_colors)     options: dict = field(default_factory=get_options)     

Здесь использовался dataclass – это относительно недавнее дополнение Python и для быстрых классов оно будет хорошим выбором. Установка начальных значений помогает гарантировать, что вы не используете изменяемый объект для экземпляра.

         def from_lists(self, values, labels, stacks):     """     Функция построения графиков из списка     ``values`` список датасетов, содержащий одно значение.     ``labels`` метки значений.     ``stacks`` метки для каждого набора данных в списке значений.     """     self.datasets = []      # убеждаемся, что у нас правильное количество цветов     if len(self.palette) < len(values):         get_random_colors(num=len(values), colors=self.palette)          # создаем датасет     for i in range(len(stacks)):         self.datasets.append(             {                 'label': stacks[i],                 'backgroundColor': self.palette[i],                 'data': values[i],             }         )      if len(values) == 1:         self.datasets[0]['backgroundColor'] = self.palette      self.labels = labels     

from_dataframe помогает использовать несколько строк кода для манипулирования практически любым фреймом данных, чтобы передавать все непосредственно в from_lists для Chart.js. Мы применяем метод pivot_table и Pandas, чтобы создать pivot_table на основе входных данных. Она может выгружать списки, которые нужны для диаграммы.

         def from_df(self, df, values, labels, stacks=None, aggfunc=np.sum, round_values=0, fill_value=0):     """     Функция построения графиков из датафрейма.     ``df`` используемый датафрейм.     ``values`` имя колонки со значениями.     ``stacks`` имя колонки со stack-ами.     ``labels`` имя колонки с метками.     ``aggfunc`` функция, агрегирующая значения.      ``round_values`` десятичный знак для округления.     ``fill_value`` используется если значение пустое.     """     pivot = pd.pivot_table(         df,         values=values,         index=stacks,         columns=labels,         aggfunc=aggfunc,         fill_value=0     )      pivot = pivot.round(round_values)      values = pivot.values.tolist()     labels = pivot.columns.tolist()     stacks = pivot.index.tolist()      self.from_lists(values, labels, stacks)     

Заключение

Чтобы внедрить код в своем проекте, скопируйте chart.py и используйте его, как views.py в нашем. Обязательно убедитесь, что базовый файл HTML импортирует chart.js cdn. Bootstrap – по желанию. Удачи в обучении и экспериментах!

Весь код из статьи:

         @dataclass class Chart:     chart_type: str     datasets: List = field(default_factory=list)     labels: List = field(default_factory=list)     chart_id: str = field(default_factory=generate_chart_id)     palette: List = field(default_factory=get_colors)     options: dict = field(default_factory=get_options)      def from_lists(self, values, labels, stacks):                 self.datasets = []          if len(self.palette) < len(values):             get_random_colors(num=len(values), colors=self.palette)                  for i in range(len(stacks)):             self.datasets.append(                 {                     'label': stacks[i],                     'backgroundColor': self.palette[i],                     'data': values[i],                 }             )          if len(values) == 1:             self.datasets[0]['backgroundColor'] = self.palette          self.labels = labels      def from_df(self, df, values, labels, stacks=None, aggfunc=np.sum, round_values=0, fill_value=0):          pivot = pd.pivot_table(             df,             values=values,             index=stacks,             columns=labels,             aggfunc=aggfunc,             fill_value=0         )          pivot = pivot.round(round_values)                  values = pivot.values.tolist()         labels = pivot.columns.tolist()         stacks = pivot.index.tolist()          self.from_lists(values, labels, stacks)      def get_elements(self):                 elements = {             'data': {                 'labels': self.labels,                  'datasets': self.datasets             },             'options': self.options         }          if self.chart_type == 'stackedBar':             elements['type'] = 'bar'             self.options['scales'] = {                         'xAxes': [                             {'stacked': 'true'}                         ],                          'yAxes': [                             {'stacked': 'true'}                         ]                     }          if self.chart_type == 'bar':             elements['type'] = 'bar'             self.options['scales'] = {                         'xAxes': [                             {                                 'ticks': {                                     'beginAtZero': 'true'                                 }                             }                         ],                          'yAxes': [                             {                                 'ticks': {                                     'beginAtZero': 'true'                                 }                             }                         ]                     }          if self.chart_type == 'groupedBar':             elements['type'] = 'bar'             self.options['scales'] = {                         'xAxes': [                             {                                 'ticks': {                                     'beginAtZero': 'true'                                 }                             }                         ],                          'yAxes': [                             {                                 'ticks': {                                     'beginAtZero': 'true'                                 }                             }                         ]                     }                  if self.chart_type == 'horizontalBar':             elements['type'] = 'horizontalBar'             self.options['scales'] = {                         'xAxes': [                             {                                 'ticks': {                                     'beginAtZero': 'true'                                 }                             }                         ],                          'yAxes': [                             {                                 'ticks': {                                     'beginAtZero': 'true'                                 }                             }                         ]                     }          if self.chart_type == 'stackedHorizontalBar':             elements['type'] = 'horizontalBar'             self.options['scales'] = {                         'xAxes': [                             {'stacked': 'true'}                         ],                          'yAxes': [                             {'stacked': 'true'}                         ]                     }          if self.chart_type == 'doughnut':             elements['type'] = 'doughnut'                  if self.chart_type == 'polarArea':             elements['type'] = 'polarArea'                  if self.chart_type == 'radar':             elements['type'] = 'radar'          return elements          def get_html(self):         code = f'<canvas id="{self.chart_id}"></canvas>'         return code      def get_js(self):         code = f"""             var chartElement = document.getElementById('{self.chart_id}').getContext('2d');             var {self.chart_id}Chart = new Chart(chartElement, {self.get_elements()})         """         return code      def get_presentation(self):         code = {             'html':self.get_html(),             'js': self.get_js(),         }         return code     

Дополнительные материалы:

  • 10 лучших материалов для изучения Django
  • Самый полный видеокурс по Django от установки до проекта
  • 10 трюков библиотеки Python Pandas, которые вам нужны
  • Python + Visual Studio Code = успешная разработка
  • Осваиваем парсинг сайта: короткий туториал на Python

  • 32 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close