Django #4

Możesz sprawdzić działanie aplikacji tworząc darmowe konto tutaj.
Kod źródłowy można obrać z Githuba tutaj.

W tym wpisie przedstawię widoki odpowiedzialne za zarządzanie autami i naprawami.

▣ Zacznę od głównego widoku prezentującego auta wpisane przez użytkownika. Maksymalna liczba wpisów na stronie to 10 aut. Po kliknięciu na poszczególne auto wyświetlane są naprawy danego auta. Zarówno wpisy aut jak i napraw są posortowane według daty tzn. najnowsze wpisy są wyświetlane jako pierwsze.

Klasa widoku wyświetlająca auta dziedziczy po klasie ListView oraz po klasie LoginRequiredMixin (aby dostęp był możliwy tylko dla zalogowanych użytkowników).

Na początku definiuję wszystkie atrybuty klasy tzn. używany model – Car, używany szablon – cars.html, nazwa obiektu, pod którą są dostępne dane w kontekście – context_object_name – ‘cars’, a także liczba wpisów na stronie – 10 pojazdów.

W tej klasie przesłaniam dwie metody – get_queryset() odpowiedzialną za filtrowanie danych oraz get_context_data(), w której uzupełniam dane kontekstu o dane z pola wyszukiwania.

Kod klasy CarListView() wygląda następująco:

class CarsListView(LoginRequiredMixin, ListView):
    model = Car
    template_name = 'cars/cars.html'
    context_object_name = 'cars'
    paginate_by = 10

    def get_queryset(self):
        if self.request.GET.get('q'):
            q = self.request.GET.get('q')
            make_results = self.model.objects.filter(
                user=self.request.user, make=q).order_by('-pk')
            model_results = self.model.objects.filter(
                user=self.request.user, model=q).order_by('-pk')
            if make_results.exists():
                return make_results
            elif model_results.exists():
                return model_results
            else:
                return self.model.objects.none()
        return self.model.objects.filter(user=self.request.user).order_by('-pk')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['q'] = self.request.GET.get('q', '')
        return context

Metoda get_queryset() wyświetla wszystkie pojazdy danego użytkownika. Ostatnio dodane auto jest widoczne jako pierwsze (sortowanie ‘-pk’). Jeśli w polu wyszukiwania została wpisana marka lub model pojazdu/pojazdów to zostaną wyświetlone tylko te pojazdy.

Metoda get_context_data() dodaje wpis z pola wyszukiwania do kontekstu, dzięki czemu poprawnie wyświetlane są dane o naprawach aut przy podziale aut na poszczególne strony.

▣ Dodawanie pojazdu odbywa się po wciśnięciu przycisku Add Car w menu aplikacji. Widok, który obsługuje dodawanie auta to klasa AddCarView(), a szablon, który wyświetla formularz dodawania auta to car_form.html (ten sam szablon obsługuje również aktualizację danych opisujących dane auto) .

Klasa AddCarView() dziedziczy funkcjonalność po klasie CreateView() oraz LoginRequiredMixin (tylko zalogowani użytkownicy mogą utworzyć nowy pojazd).

Definiuję następujące atrybuty klasy widoku: model, który używa klasa – Car, fields – pola formularza, które mają być widoczne, oraz success_url – adres url, który zostanie załadowany po pomyślnym wypełnieniu formularza.

Przesłaniam metodę form_valid(), która dodaje użytkownika, który utworzył nowe auto (model Car wymaga zdefiniowania atrybutu user).

Kod klasy AddCarView() wygląda następująco:

class AddCarView(LoginRequiredMixin, CreateView):
    model = Car
    fields = ['make', 'model', 'vrn', 'year']
    success_url = '/'

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)

▣ Usuwanie auta jest realizowane za pomocą klasy widoku DeleteCarView(), a szablon, który wyświetla monit czy kasować auto to plik car_confirm_delete.html (wyświetla przycisk Delete, który kasuje auto i przycisk Cancel, który wraca do poprzedniej strony).

Klasa DeleteCarView() dziedziczy funkcjonalność po klasie DeleteView, LoginRequiredMixin (tylko zalogowany użytkownik może skasować auto) oraz UserPassesTestMixin(użytkownik może usunąć tylko auto utworzone przez siebie).

Definiuję argumenty model – używany model Car oraz success_url – adres, pod który będzie załadowany po pomyślnym usunięciu auta – w tym przypadku widok prezentujący wszystkie auta użytkownika.

Tworzę metodę test_func(), która sprawdza, czy użytkownik, który chce usunąć auto jest osobą, która utworzyła dane auto (użytkownik może usunąć tylko utworzone przez siebie auta).

Przesłaniam metodę delete(), która dodatkowo wyświetla wiadomość o skasowaniu auta.

Kod klasy DeleteCarView() wygląda następująco:

class DeleteCarView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Car
    success_url = '/'

    def test_func(self):
        if self.get_object().user == self.request.user:
            return True
        return False

    def delete(self, request, *args, **kwargs):
        success_message = f'Car {self.get_object()} has been deleted'
        messages.success(self.request, success_message)
        return super().delete(request, *args, **kwargs)

▣ Uaktualnienie danych opisujących konkretne auto odbywa się za pomocą klasy UpdateCarView(), natomiast szablon wyświetlający formularz do zmiany tych danych to car_form.html.

Klasa UpdateCarView() dziedziczy funkcjonalność po klasie UpdateView, jak również po klasach LoginRequiredMixin (uaktualnienie danych może dokonać tylko zalogowany użytkownik) oraz UserPassesTestMixin (wywoływana jest funkcja sprawdzająca, czy użytkownik, który chce dokonać modyfikacji jest tym, który utworzył dane auto).

Definiuję atrybuty klasy: model – określa model, który jest wykorzystywany do zmiany danych – w tym przypadku model Car. Atrybut fields – określający jakie pola formularza mają być dostępne. Atrybut success_message definiujący łańcuch tekstowy do wyświetlenia jako komunikat o uaktualnieniu danych auta.

Definiuję dwie metody: test_func() oraz get_success_url().

Metoda test_func() sprawdza, czy użytkownik, który chce dokonać modyfikacji danych auta jest tym, który utworzył dane auto.

Metoda get_success_url() przesłania metodę dokonującą modyfikacji danych auta i dodatkowo wyświetla komunikat o zmianie danych i wraca na stronę określoną nazwą: car_detail – wyświetlającą naprawy dla danego auta. Jako dodatkowe parametry przesyłane są za pomocą metody GET: row, p oraz q określające odpowiednio wiersz, stronę oraz łańcuch tekstowy z pola wyszukiwania.

Kod klasy UpdateCarView():

class UpdateCarView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Car
    fields = ['make', 'model', 'vrn', 'year']
    success_message = 'Car info has been updated'

    def get_success_url(self, **kwargs):
        row = self.request.GET.get('row')
        p = self.request.GET.get('p')
        q = self.request.GET.get('q')
        options = '?p=' + p + '&row=' + row
        options += '&q=' + q
        messages.success(self.request, self.success_message)
        return reverse_lazy('car_detail') + options

    def test_func(self):
        if self.get_object().user == self.request.user:
            return True
        return False

▣ Dodanie notatki o naprawie jest definiowane w klasie AddRepairView() widoku, a formularz jest zdefiniowany w szablonie repair_form.html.

W klasie tej występują następujące argumenty: model – określający wykorzystywany model – w tym przypadku Repair. Następny argument: fields określa jakie są wyświetlane pola formularza. Ostatni argument to łańcuch success_message wyświetlany po pomyślnym dodaniu notatki o naprawie.

Metody obecne w klasie AddRepairView() to: get_context_data(), form_valid() oraz get_success_url().

Metoda get_context_data() dodaje obiekt Car do kontekstu, dzięki czemu jest on widoczny w szablonie.

Metoda form_valid() wykorzystuje dane z formularza do utworzenia nowej instancji modelu. Do poprawnego utworzenia instancji klasy Repair jest konieczne podanie klucza obcego – obiektu Car określającego pojazd, którego dotyczy dana notatka o naprawie.

Metoda get_success_url() określa adres strony, która ma być wyświetlona po pomyślnym dodaniu nowej notatki. W tym przypadku wyświetlona zostanie strona o nazwie car_detail zdefiniowana w cars/urls.py.

class AddRepairView(LoginRequiredMixin, CreateView):
    model = Repair
    fields = ['date', 'description']
    success_message = 'New repair has been added'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['car'] = Car.objects.get(id=self.kwargs['pk'])
        return context

    def form_valid(self, form, **kwargs):
        form.instance.car = Car.objects.get(id=self.kwargs['pk'])
        return super().form_valid(form)

    def get_success_url(self, **kwargs):
        row = self.request.GET.get('row')
        p = self.request.GET.get('p')
        q = self.request.GET.get('q')
        options = '?p=' + p + '&row=' + row
        options += '&q=' + q
        return reverse_lazy('car_detail') + options

— c.d.n —

Matplotlib – osadzanie wykresu w aplikacji #2

Drobne zmiany w kodzie.

Wykresy zapisywane są na lokalnym dysku, w katalogu data/ projektu. Jeśli na dysku jest już zapisany wykres dla danego roku, to jest on wczytywany z lokalnych zasobów, bez tworzenia zapytań do serwera, w celu przyspieszenia wyświetlania wykresu.

Alternatywnie można byłoby pobrać wszystkie dane za jednym razem asynchronicznie np. korzystając z obiektu ThreadPoolExecutor() i następnie odwoływać się już tylko do lokalnych zasobów.

Matplotlib – osadzenie wykresu w aplikacji

Kod programu można pobrać tutaj.

Okno programu zostało utworzone w bibliotece tkinter. Na wykres została przewidziana osobna ramka tkinter.Frame(). W ramce tej został umieszczony wykres dzięki zastosowaniu obiektu FigureCanvasTkAgg. Obiekt ten jako parametry przyjmuje obiekt Figure oraz kontener – w tym przypadku obiekt Frame.

W oknie programu umieściłem podstawowe widżety tzn. menu programu, pole wprowadzania i przycisk tkinter.Button() umożliwiające zmianę roku, dla którego generowany jest wykres oraz pole statusu, w którym są wyświetlane komunikaty o poprawnym wczytaniu danych lub o braku danych do wygenerowania wykresu dla danego roku.

Domyślnie po uruchomieniu programu wyświetlany jest wykres cen hurtowych dla bieżącego roku.

Okno programu generującego wykres cen hurtowych paliw na podstawie podanego roku.

Matplotlib i parsowanie HTML

W dzisiejszym wpisie przedstawię parsowanie strony Orlenu w celu pobrania danych o zmianach cen hurtowych oleju napędowego i przedstawienia tych zmian na wykresie.

Na wstępie niezbędne jest zaimportowanie bibliotek: requests – w celu obsługi zapytań HTLM, lxml .html do parsowania HTML, matplotlib.pyplot – w celu wyświetlenia wykresu na podstawie sparsowanych danych oraz unicodedata – w celu obsługi unicode.

import requests
from lxml import html
import matplotlib.pyplot as plt
import unicodedata

if __name__ == "__main__":
    main()

def main():
   print_chart(my_parser(2019))    #wyświetla wykres dla 2019 roku

def print_chart(content):
    year = content.get('year')  
    fuel = content.get('fuel')
    dates = content.get('dates') 
    prices = content.get('prices')

    plt.style.use('bmh')

    fig, ax = plt.subplots()

    ax.set_title(f'Cena dla paliwa {fuel} - {year} rok')
    ax.set_xlabel('Data')
    ax.set_ylabel('Cena')
    fig.autofmt_xdate()
    ax.grid(True)
    ax.xaxis.set_major_locator(plt.MaxNLocator(10))
    ax.plot(dates, prices, c='#CA3F62')
    plt.show()

Powyższa funkcja rysuje wykres liniowy zmian cen hurtowych oleju napędowego w danych roku. Argument content przekazywany do funkcji jest słownikiem. Wartości dla kolejnych kluczy słownika są przekazywane do zmiennych year, fuel, dates, i prices. Wartości dates i prices są listami przechowującymi odpowiednio daty i ceny hurtowe Orlenu.

Następnie w wierszu plt.style use(‘bmh’) zostaje użyty styl graficzny dla wykresu w celu poprawy wyglądu wykresu. Listę dostępnych stylów można wyświetlić za pomocą: print(plt.style.available).

Następnie za pomocą jako wynik funkcji plt.subplots() zostaje zwrócona krotka, której wartości są przypisane do zmiennych – odpowiednio fig oraz ax. Zmienna fig przechowuje obiekt Figure, który można utożsamiać z oknem wszystkich wykresów, natomiast ax przechowuje obiekt AxesSubplot, który możemy utożsamiać z poszczególnym wykresem, jednakże na takim obiekcie może być rysowanych kilka wykresów dla różnych danych np. obiekt taki może przechowywać wykres zmian cen oleju napędowego oraz drugi wykres, zawierający cenę etyliny w tym okresie. Jeżeli chcielibyśmy przedstawić oba wykresy w tym samym oknie, w tym samym obiekcie Figure, ale np. w dwóch kolumnach, to należałoby użyć dwóch obiektów ax np. ax1 i ax2 tzn.

fig, (ax1, ax2) = plt.subplots(rows=1, cols=2)

W kolejnych wierszach ustawiany jest tytuł dla wykresu, etykieta dla osi X i etykieta dla osi Y. Następnie wywołuję funkcję autofmt_xdate(), aby poprawić widoczność etykiet dla osi X – etykiety wyświetlane są pod kątem i nie przesłaniają się nawzajem.

Kolejnym krokiem jest wyświetlenie siatki wykresu za pomocą funkcji grid() przekazując parametr True – w celu poprawy wyglądu wykresu.

Można zauważyć, że dane osi X przesłaniają się mimo wszystko. Aby poprawić widoczność wartości na osi x ograniczyłem ilość wyświetlanych wartości na osi X do 10 za pomocą funkcji: ax.xaxis.set_major_locator(plt.MaxNLocator(10))

W kolejnym wierszu: ax.plot(dates, prices, c=’#CA3F62′) tworzony jest wykres liniowy na podstawie danych dla osi X – zawartych w liście dates oraz dla danych dla osi Y zawartych w liście prices. Parametr c określa kolor linii wykresu.

Funkcja show() w kolejnej linii wyświetla tak przygotowany wykres.

Słownik content przekazany jako argument do funkcji print_chart() jest zwracany jako wynik funkcji my_parser(), która jako parametr przyjmuje argument year – rok, dla którego chcemy pobrać dane. Kod mojej funkcji my_parser() wygląda następująco:

def my_parser(year):
    page = requests.get(f'https://www.orlen.pl/PL/DlaBiznesu/HurtoweCenyPaliw/Strony/archiwum-cen.aspx?Fuel=ONEkodiesel&Year={year}')
    text = unicodedata.normalize("NFKC", page.text)
    tree = html.fromstring(text)
    table_content = tree.xpath('//tr/td/span/text()')
    table_content = table_content[::-1]
    content = {}
    content['year'] = year
    content['fuel'] = table_content.pop()
    content['dates'] = table_content[1::2]
    prices = table_content[::2]
    prices = [price.replace(' ', '') for price in prices]
    prices = list(map(int, prices))
    content['prices'] = prices
    return content

Najpierw za pomocą funkcji get() z biblioteki requests jest tworzone zapytanie strony Orlenu dla danego roku, podanego jako parametr funkcji my_parser(). W efekcie zmienna page przechowuje zwrócony obiekt Response.

W kolejnym etapie zawartość strony jest normalizowana, aby pozbyć się z tekstu kodów unicode.

W linii tree = html.fromstring(text) tworzona jest zmienna tree, za pomocą której następnie będzie wyszukiwana zawartość tabeli, przechowującej daty i ceny paliwa tzn.:

table_content = tree.xpath(‘//tr/td/span/text()’) # pobranie wartości z tabeli

Pobrana zawartość z tabeli jest listą zawierającą kolejno: nazwę paliwa, a następnie na przemian daty i ceny paliwa. Dane są posortowane od końca roku do początku i aby uzyskać odwrotne wartości należy wpisać:

table_content = table_content[::-1]

Dzięki temu dane z początku roku będą na początku wykresu.

Kolejnym krokiem jest utworzenie słownika content, którego kluczami będą: year, fuel, dates i prices, przechowującymi odpowiednio rok, dla którego chcemy wyświetlić dane; nazwę paliwa; zawierający listę dat i listę cen hurtowych. Wartość year jest przekazywana na podstawie parametru funkcji my_parser(). Wartość fuel jest pobrana za pomocą funkcji pop() z listy table_content – dzięki czemu docelowo lista ma tylko dane zawierające daty i ceny. W liście prices jest są dokonywane czynności porządkowe tzn. usunięcie spacji oddzielających tysiące oraz zrzutowanie tak zmodyfikowanych łańcuchów na typ int.

Tak utworzony słownik jest następnie zwracany jako argument do funkcji print_chart()

W efekcie wykres cen hurtowych dla oleju napędowego dla przykładowego roku 2019 wygląda następująco:

Wykres hurtowych cen oleju napędowego – PKN Orlen w 2019 roku.

W kolejnym wpisie opiszę jak osadzić wykres utworzony z danych pobranych ze strony PKN Orlen we własnej aplikacji na podstawie opisanej już wcześniej biblioteki tkinter.

— c. d. n. —

Mapy kolorów w matplotlib

Aby utworzyć wykres punktowy, korzystający z wybranej palety kolorów należy jako argument cmap podać nazwę wybranego schematu kolorów. Kolory pogrupowane są w grupy >>dokumentacja<< – np. Sequential – różne natężenie jednego koloru, Diverging – różne natężenia dwóch kontrastowych kolorów, Qualitative – kontrastowe różne kolory, ale pasujące do palety np. Pastel1 – zawierający różne kolory pastelowe.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()

ax.set_title('Tytuł wykresu')
ax.set_xlabel('Etykieta osi X')
ax.set_ylabel('Etykieta osi Y')

# x - lista z wartościami osi X np. nazwa produktu
# y - lista z wartościami osi Y np. cena produktu
# intensivity - lista z liczbami odpowiadającymi instensywności danej cechy
# cm - nazwa wybranej palety kolorów

ax.scatter(x, y, c=intensivity,  cmap=cm)  
  # Przykładowo: plt.scatter(x, y, c=intensivity, s=50,  cmap='plasma')

mappable = ax.collections[0]
cbar = fig.colorbar(mappable=mappable)
cbar.set_label('intensivity')

plt.show()

Parametr s odpowiada za wielkość rysowanych znaków.

Aby wyświetlić pasek colorbar, który obrazuje intensywność danej cechy dla wyświetlonego punktu należy jako parametr podać obiekt mappable, który dla wykresu punktowego jest przechowywany jako element listy collections obiektu AxesSubplot.

W efekcie na wykresie, który może przedstawiać np. nazwę produktu i cenę można umieścić dodatkową informację za pomocą koloru np. popularność wśród kupujących lub ilość towaru w magazynie (np. kolor zielony – towar dostępny bez problemu aż po kolor czerwony – brak towaru w magazynie)

Pandas – filtrowanie, grupowanie, eksport do Excela

Na podstawie pliku listy tankowań pobranego w formacie csv przedstawię podstawowe możliwości biblioteki Pandas. We wcześniejszym >>artykule<< opisałem parsowanie za pomocą modułu csv z biblioteki standardowej Pythona.

Plik csv jest plikiem tekstowym, w którym wartości poszczególnych kolumn są oddzielone delimiterem, najczęściej znakiem przecinka. W tym konkretnym przypadku, w pobranym pliku raportu listy tankowań, wartości są oddzielone od siebie znakiem średnika.

Nagłówek danych ma postać: Dane Kontrahenta;Imie;Nazwisko;Numer Korekty;Numer WZ;Data;Godzina;Licznik;Stacja;Numer Rejestracyjny;Numer Karty;Nazwa towaru;VAT procent;Cena bazowa;Cena netto;Cena brutto;Wartość rabatu;Ilość;Netto;VAT;Brutto. Kolejne wiersze zawierają wiersze z danymi dotyczącymi kolejnych tankowań.

Aby wczytać pobrany plik csv o nazwie report.csv musimy utworzyć obiekt ramki danych, gdzie pierwszym argumentem jest ścieżka do pliku, który chcemy wczytać, a argument sep określa rodzaj separatora, tzn.

import pandas as pd

df = pd.read_csv('path/to/report.csv', sep=';')

▣ Na podstawie utworzonego obiektu możemy np. określić ilość poszczególnych wierszy danych np. ilość tankowań z podziałem na różne paliwa tzn.

refuel_amount = df['Nazwa towaru'].value_counts()

Przykładowy wynik :

OLEJ NAPĘDOWY                     466
OPŁATA ZA WYDANIE KARTY FLOTA      1
Name: Nazwa towaru, dtype: int64

▣ Jeśli chcemy np. określić średnią cenę netto dla paliwa zawierającego w nazwie łańcuch: ‘napędowy’ to musimy zastosować filtrowanie tzn.

filter_ = df['Nazwa towaru'].str.lower().str.contains('napędowy')

result = df[filter_]

(Oryginalnie zawartość kolumny Nazwa towaru była pisana dużymi literami, stąd zastosowanie metody lower()). W wyniku zastosowania filtra ramka danych o nazwie result posiada tylko dane związane z wybranym paliwem. Niestety, dane wpisane w kolumnie Cena netto posiadają części dziesiętne oddzielone za pomocą przecinka a nie kropki. Aby dokonać konwersji wszystkich danych z kolumny Cena netto zastosuję metodę replace() a następnie konwersję wartości z kolumny na typ float tzn.

result['Cena netto'] = result['Cena netto'].str.replace(',', '.')

result['Cena netto'] = result['Cena netto'].astype(float)

Średnia cena paliwa dla kolumny Cena netto ramki result utworzonej w efekcie zastosowania filtra:

result['Cena netto'].mean()       # wylicza średnią cenę danego paliwa

Analogicznie stosując funkcje min(), max() możemy wyliczyć najniższą lub najwyższą cenę paliwa w danym okresie dla wybranego paliwa.

▣ Jeśli chcemy obliczyć średnią cenę dla różnych rodzajów paliw z kolumny Nazwa towaru to musimy zastosować grupowanie. Utworzę zmienną fuel przechowującą unikalne wartości z kolumny Nazwa towaru tzn.

fuel = df.groupby(['Nazwa towaru']                # obiekt DataFrameGroupBy

fuel['Cena netto'].mean()        # Zwraca średnią cenę netto dla każdego rodzaju paliwa

W powyższym przykładzie także należy mieć na uwadze, aby kolumna Cena netto zawierała elementy typu float .

▣ Jeśli chcielibyśmy przeszukiwać dane na podstawie daty to można zauważyć, że w pobranym pliku csv mamy dwie kolumny odpowiedzialne za przechowywanie daty i godziny. Są to odpowiednio kolumna Data oraz kolumna Godzina. Dane o wpisach do rejestru tankowań są posortowane, ale jeśli chcielibyśmy znaleźć np. tankowania z określonego zakresu dat lub np. średnią cenę z określonego zakresu dat, to należałoby zamienić wartości z tych obu kolumn na obiekt typu datetime. W poniższym przykładzie utworzę nową kolumnę, która będzie przechowywać wartość typu datetime dla każdego wiersza ramki danych.

df['datetime'] = df['Data'] + ' ' + df['Godzina']    # na razie kolumna z wartościami tekstowymi typu object 

df['datetime'] = df['datetime'].apply(lambda x: datetime.strptime(x, '%Y.%m.%d %H:%M:%S'))

Dla każdej wartości wiersza kolumny datetime zostaje zastosowana funkcja strptime(), która zamienia łańcuch znaków mający postać np. 2020.04.20 22:20:10 w obiekt typu datetime zgodnie z formatem podanym jako drugi argument funkcji.

Wartości z kolumny datetime mogą posłużyć jako index ramki, co ułatwi wyszukiwanie za pomocą wycinków (slices) tzn.

df.set_index('datetime', inplace=True)  # argument inplace=True nadpisuje ramkę df

Jeśli chcemy np. wyświetlić wiersze z kolumn Nazwa towaru oraz Cena netto dla wierszy z zakresu dat np. od 15 do 20 kwietnia 2020 to możemy napisać:

df.loc['2020-04-15':'2020-04-20', ['Nazwa towaru','Cena netto']]

▣ Jeśli chcemy wyświetlić maksymalną i minimalne cenę wszystkich rodzajów paliw z pobranego raportu w tygodniach objętych raportem, to możemy użyć metodę resample():

fuel = df.groupby(['Nazwa towaru'])  # utworzenie grupy
  # z wszystkich rodzajów paliw 
  # występujących w kolumnie 'Nazwa towaru' (unikalne wartości)

fuel['Cena netto'].resample('W').agg(['max', 'min'])  

W powyższym przykładzie utworzyłem najpierw zmienną przechowującą obiekt DataFrameGroupBy, przechowującą unikalne wartości z wierszy tankowań. Następnie dla każdego unikalnego elementu wyświetlam wartości ceny minimalnej i maksymalnej zwracane przez metody min() oraz max(), ale wyniki są wyświetlane dla okresów tygodniowych (argument ‘W’ – week). Analogicznie dla interwałów dwutygodniowych wystarczy jako argument użyć ‘2W’.

Przykładowy wynik :

		                max	min
Nazwa towaru	 datetime		
OLEJ NAPĘDOWY 	2020-04-05	3.39	3.24
                2020-04-19	3.20	2.98
                2020-05-03	2.99	2.87
                2020-05-17	3.11	2.94
                2020-05-31	3.21	3.11
OPŁATA ZA WYDANIE KARTY 2020-05-19 4.07	4.07

▣ Jeśli chcielibyśmy wyświetlić dane dotyczące okresów tygodniowych, ale dla wartości wierszy z kolumny ‘Cena netto’ interesuje nas, jaka w tych tygodniach jest maksymalna i minimalna cena netto, a dla kolumny ‘Ilość’, reprezentującej ilość zatankowanego paliwa chcielibyśmy uzyskać informację o ilości zakupionego paliwa w danym tygodniu:

result = df.resample('W').agg({'Cena netto':['min', 'max'], 'Ilość':'sum'})

W powyższym przykładzie stosujemy funkcję agregującą agg(), która jako argument pobiera słownik, w którym indeksami są nazwy kolumn, a wartościami nazwy funkcji, które chcemy zastosować.

Przykładowy wynik :

	          Cena netto	Ilość
                min	max	sum
datetime			
2020-04-05	3.01	3.11	2969.00
2020-04-12	3.08	3.15	3019.54
2020-04-19	2.88	3.05	3808.03
2020-04-26	2.81	4.02	3902.50
2020-05-03	2.78	2.89	3808.00
2020-05-10	2.74	2.95	4409.00
2020-05-17	2.95	3.05	4008.04
2020-05-24	3.02	3.15	4684.05
2020-05-31	3.12	3.18	4055.00

▣ Zapis lub odczyt z plików Excela:

df = pd.read_excel('path/to/file.xlsx')  # odczyt z pliku file.xlsx

df.to_excel('path/to/file.xlsx')  # zapis do pliku file.xlsx

Aby odczytać/zapisać dane z konkretnego arkusza pliku programu Excel, to jako drugi parametr należy podać argument sheet_name

df.to_excel('path/to/file.xlsx', sheet_name='Arkusz_1')    # zapisuje dane z ramki
 # do pliku file.xlsx w arkuszu o nazwie Arkusz_1

Pandas – wyszukiwanie danych

  • Dwa alternatywne sposoby wyświetlenie danych z konkretnej kolumny i wiersza ramki (w tym przypadku wiersz o indeksie 0 i kolumna o nazwie name)
df.loc[0, "name"]
lub
df.name.loc[0]
  • Wyszukiwanie danych w ramce za pomocą filtra (składnia: df[filtr])
df[df.col_name == "name"]    # zwraca wiersze zawierające wartość name w kolumnie col_name
lub
df[df["col_name"] == "name"] # zwraca wiersze zawierające wartość name w kolumnie col_name

Zawężenie wyniku do wybranych kolumn. Wynikiem są wiersze zawierające wartość name w kolumnie col_name, przy czym wynik jest ograniczony do wyświetlenia wartości z kolumn col_x oraz col_y

df.loc[df.col_name == "name", ["col_x", "col_y"]]
lub
df.loc[df["col_name"] =="name", ["col_x", "col_y"]]

Dekoratory z parametrami – gra Fizz Buzz

Gra Fizz Buzz to klasyczne zadanie – test na odfiltrowanie z ciągu liczb tych, które są podzielne przez zadane liczby bez reszty tzn. jeśli mamy np. ciąg od 0 do 5 i szukamy takich liczb, które dzielą się np. przez 3 i przez 5 bez reszty, to kolejno otrzymujemy: dla 0 łańcuch FIZZBUZZ, dla 1 pusty łańcuch, dla 2 pusty łańcuch, dla 3 otrzymujemy FIZZ, dla 4 mamy pusty łańcuch i dla 5 mamy BUZZ.

Najprościej możemy to rozwiązać w następujący sposób:

def fizzbuzz(start, end, fizz, buzz):
    """ Sample fizzbuzz game"""
    for number in range(start, end + 1):
        if not number % fizz and not number % buzz:
            print(number, 'FIZZBUZZ')
            continue
        elif not number % fizz:
            print(number, 'FIZZ')
            continue
        elif not number % buzz:
            print(number, 'BUZZ')
            continue
        print(number)

Moje rozwiązanie zadania za pomocą dekoratorów wygląda następująco:

def fizz(fizz_number):
    def decor(func):
        def wrapper(*args):
            dict = func(*args)
            for key in dict:
                if not (key % fizz_number):
                    dict[key] += 'FIZZ'
            return dict
        return wrapper
    return decor

Analogicznie funkcja buzz() wygląda następująco:

def buzz(buzz_number):
    def decor(func):
        def wrapper(*args):
            dict = func(*args)
            for key in dict:
                if not (key % buzz_number):
                    dict[key] += 'BUZZ'
            return dict
        return wrapper
    return decor

Utworzę jeszcze funkcję numbers(), która będzie zwracała słownik o kluczach będących liczbami o zakresie podanym jako argumenty tzn.

def numbers(start, end):
    dict = {}
    for number in range(start, end + 1):
        dict[number] = ''
    return dict

Powyższą funkcję możemy zmodyfikować za pomocą utworzonych wcześniej dekoratorów z parametrami tzn.

@buzz(5)
@fizz(3)
def numbers(start, end):
    dict = {}
    for number in range(start, end + 1):
        dict[number] = ''
    return dict

Zmodyfikowany słownik możemy wyświetlić za pomocą wywołania zmodyfikowanej poprzez użycie dekoratorów funkcji numbers() tzn.

print(numbers(0, 15))

Można zauważyć, że obie funkcje dekoratora są bardzo podobne, więc aby uniknąć redundancji kodu możemy obie funkcje zastąpić jedną:

def fizzbuzz(number, message):
    def decor(func):
        def wrapper(*args):
            dict = func(*args)
            for key in dict:
                if not (key % number):
                    dict[key] += message
            return dict
        return wrapper
    return decor

Definicja funkcji numbers() po dodaniu dekoratora będzie wyglądać następująco:

@fizzbuzz(5, 'BUZZ')
@fizzbuzz(3, 'FIZZ')
def numbers(start, end):
    dict = {}
    for number in range(start, end + 1):
        dict[number] = ''
    return dict

Django #3

We wcześniejszych wpisach opisałem zarządzanie użytkownikami w >>aplikacji<< tzn. nowy użytkownik może się zarejestrować – utworzyć konto w witrynie, użytkownik zarejestrowany może się zalogować, wylogować lub zmienić swoje hasło.

Następnie opiszę główną funkcjonalność witryny tzn. ewidencję aut i napraw. W tym celu w pliku urls.py projektu załączę mapowanie adresów związanych z aplikacją cars. Po zmianach plik urls.py projektu będzie wyglądał następująco:

from django.contrib import admin
from django.urls import path, include
from users import views as users_views
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('cars.urls')),
    path('register-user/', users_views.RegisterUser.as_view(
        template_name='users/register-user.html'), name='register_user'),
    path('login/', auth_views.LoginView.as_view(template_name='users/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(template_name='users/logout.html'), name='logout'),
    path('change-password/', users_views.ChangePassword.as_view(
        template_name='users/change-password.html'), name='change_password'),
]

Można zauważyć, że mapowanie adresów na odpowiednie widoki uwzględnia też plik urls.py utworzony w aplikacji cars. Plik urls.py z aplikacji cars wyglada następująco:

from django.urls import path
from .views import (
    CarsListView,
    AddCarView,
    RepairsListView,
    UpdateCarView,
    DeleteCarView,
    AddRepairView
)
urlpatterns = [
    path('', CarsListView.as_view(), name='cars'),
    path('add-car/', AddCarView.as_view(), name='add_car'),
    path('car/', RepairsListView.as_view(), name='car_detail'),
    path('car/<int:pk>/update/', UpdateCarView.as_view(), name='update_car'),
    path('car/<int:pk>/delete/', DeleteCarView.as_view(), name='delete_car'),
    path('car/<int:pk>/new-repair/', AddRepairView.as_view(), name='add_repair'),
]

Główny widok definiowany w klasie CarListView wyświetla wszystkie auta użytkownika.

Należy pamiętać o dopisaniu aplikacji w pliku settings.py projektu w liście INSTALLED_APPS np. dla aplikacji cars będzie to element: ‘cars.apps.CarsConfig’.

W aplikacji users korzystałem z preinstalowanego modelu użytkownika. W aplikacji cars należy stworzyć modele – obiekty mapujące tabele bazy danych. Modele te będą definiować: pojazd – model Car i naprawę – model Repair. Kod pliku models.py aplikacji cars wygląda następująco:

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.validators import MaxValueValidator, MinValueValidator


class Car(models.Model):
    make = models.CharField(max_length=10)
    model = models.CharField(max_length=10)
    vrn = models.CharField(max_length=10)
    year = models.IntegerField(default=timezone.now().year,
                               validators=[MinValueValidator(timezone.now().year - 100),
                                           MaxValueValidator(timezone.now().year)])
    user = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f'{self.make} {self.model}'


class Repair(models.Model):
    date = models.DateField(default=timezone.now)
    description = models.TextField()
    car = models.ForeignKey(Car, on_delete=models.CASCADE)

    def __str__(self):
        lead = self.description[:10] + '...'
        return lead

Model Car zawiera atrybuty make, model, vrn, year i user opisujące markę pojazdu, model, nr rejestracyjny pojazdu i rok produkcji, a także klucz obcy, zawierający obiekt użytkownika, który utworzył wpis o pojeździe. Pole year posiada walidatory, uniemożliwiające wpisanie auta o roku produkcji o 100 lat starszym od bieżącej daty i nowszym niż bieżący rok. Opcja on_delete atrybutu user ma wartość domyślną CASCADE tzn. usunięcie użytkownika, który wprowadzić powoduje także usunięcie wszystkich wprowadzonych przez niego aut.

Model Repair zawiera atrybuty date – data naprawy, description – szczegóły naprawy oraz car, będący kluczem obcym – jakiego auta dotyczy dana naprawa. Metoda __str__() wyświetla nagłówek będący początkiem opisu naprawy (10 początkowych znaków).

Aby móc dokonywać edycji wpisów obu modeli z poziomu panelu administracyjnego trzeba dopisać oba modele do pliku admin.py aplikacji cars tzn.

from django.contrib import admin
from .models import Car, Repair

admin.site.register(Car)
admin.site.register(Repair)

W następnej części opiszę widoki definiujące poszczególne funkcjonalności tzn. wyświetlanie aut należących do użytkownika, dodawanie napraw, a także wyszukiwanie aut za pomocą pola wyszukiwania na stronie.

— c. d. n. —

car search
car search