„Click” i już – eleganckie aplikacje konsolowe w pythonie

TLDR: pakiet Click pozwala na przygotowanie w łatwy sposób aplikacji konsolowej, zarządzając m.in. parametrami

Autor zdjęcia: https://www.pexels.com/@negativespace

Czasami gdy przygotowaliśmy jakiś przydatny (bądź nie) kawałek kodu, chcielibyśmy go udostępnić światu. Dobrze wówczas zadbać o wygodny interfejs do jego wykorzystywania. Jeżeli ma być to aplikacja konsolowa można w tym celu wykorzystać pakiet click

pip install click

Załóżmy, że korzystając z pakietu basketball_reference_web_scraper chcemy „wydrukować” personalia zawodnika zdobywającego najwięcej punktów w NBA – przygotowaliśmy sobie skrypt dla bieżącego sezonu.

from basketball_reference_web_scraper import client

players = pd.DataFrame(client.players_season_totals(season_end_year=2020))
players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

max_player = players.iloc[players['points'].idxmax()]
print("name:", max_player['name'], "-", "points", "in", "2020", "season:",  max_player['points'])

Automatyzacja tego zadania dla wielu sezonów do postaci wykonywalnego skryptu jest bajecznie prosta:

import click
from basketball_reference_web_scraper import client
import pandas as pd

@click.command()
@click.argument('season_end_year', type=int)
def max_player(season_end_year):
    """Print name and number of points in of player with most points in NBA season which ends in SEASON_END_YEAR 

    SEASON_END_YEAR is a int number
    """
    players = pd.DataFrame(client.players_season_totals(season_end_year=season_end_year))
    players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
    players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

    max_player = players.iloc[players['points'].idxmax()]
    print("name:", max_player['name'])
    print("points", "in", season_end_year-1, "/", season_end_year, "season:",  max_player['points'])

if __name__ == '__main__':
    max_player()

@click.command() to dekorator który zamienia funkcję poniżej w obiekt command – nie zagłębiając się w szczegóły, to konieczne aby następne polecenia zadziałały.
@click.argument() definiuje argument skryptu – wartość wpisywaną po nazwie skryptu.
Wywołanie skryptu jest już bardzo proste:

python max_player.py 2010

Co zwraca nam informację o ówczesnym królu strzelców:

name: Kevin Durant
points in 2009 / 2010 season: 2472

Proste jest także dodawanie „parametrów” skryptu – załóżmy, że chcemy dodać możliwość sprawdzania innych statystyk niż punkty. Służy do tego dekorator @click.option()

import click
from basketball_reference_web_scraper import client
import pandas as pd

@click.command()
@click.option('--stat', '-s', type=click.STRING, help="Stat used to determine max player", default="points")
@click.argument('season_end_year', type=int)
def max_player(season_end_year, stat):
    """Print name and number of points (assists, rebounds) of player with most points (assists, rebounds) in NBA season which ends in SEASON_END_YEAR 

    SEASON_END_YEAR is an int number
    """

    if stat not in ("points", "assists", "rebounds", "steals", "blocks"):
        print("stat not recognized")

    players = pd.DataFrame(client.players_season_totals(season_end_year=season_end_year))
       
    players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
    players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

    max_player = players.iloc[players[stat].idxmax()]
    print("name:", max_player['name'])
    print(stat, "in", season_end_year-1, "/", season_end_year, "season:",  max_player['points'])

if __name__ == '__main__':
    max_player()

Dzięki zastosowaniu tego możemy sprawdzić np. kto był królem asyst w 2015…

python max_player.py --stat=assists 2015

i przypomnieć sobie „złotą erę” Chrisa Paula:

name: Chris Paul
assists in 2014 / 2015 season: 1564

Swoistą wisienką na torcie jest automatyczne generowanie helpa – wywołanie:

python max_player.py --help

zwróci nam ładnie sformatowany manual

Usage: max_player.py [OPTIONS] SEASON_END_YEAR

  Print name and number of points (assists,
  rebounds) in of player with most points in NBA
  season which ends in SEASON_END_YEAR

  SEASON_END_YEAR is a int number

Options:
  -s, --stat TEXT  Stat used to determine max
                   player

  --help           Show this message and exit.

Zastosowań clicka jest oczywiście dużo, dużo więcej – choćby wprowadzanie haseł ale o tym już najlepiej poczytać na stronie dokumentacji

Logowanie pandas – pandas-log

TLDR: pandas-log umożliwia logowanie, przydatne zwłaszcza w długich łańcuchach operacji w pandas

Autorka zdjęcia: https://www.pexels.com/@rethaferguson

Pandas to wspaniała biblioteka, zwłaszcza dla osób, które miały doświadczenie w SQLu i chcą je wykorzystać, pracując z danymi w Pythonie. Pozwala na pracę na tabelarycznych danych w wygodny sposób.

Podobnie jednak jak w SQLu, czasami przychodzi moment, gdy podczas przetwarzania dzieje się coś dziwnego i nie do końca wiadomo, co poszło źle.

Załóżmy że pracujemy na tym samym datasecie graczy NBA z tego sezonu co w poprzednim wpisie i chcemy znaleźć drużynę, której pozycja centra jest najsilniej obsadzona.

import pandas as pd
from basketball_reference_web_scraper import client

players = pd.DataFrame(client.players_season_totals(season_end_year=2020))
players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

columns = ['made_field_goals', 'attempted_field_goals', 'made_three_point_field_goals',
       'attempted_three_point_field_goals', 'made_free_throws',
       'attempted_free_throws', 'offensive_rebounds', 'defensive_rebounds',
       'assists', 'steals', 'blocks', 'turnovers', 'personal_fouls', 'points']

for stat in columns:
  players[stat+"_per_game"] = players[stat] / players["games_played"]

for stat in columns:
  players[stat+"_per_36"] = players[stat] / players["minutes_played"] * 36.

Mając przygotowany dataset, chcemy przeprowadzić kilka operacji:
– wybrać graczy, którzy są centrami
– odfiltrować graczy, którzy rozegrali mniej niż 15 spotkań (gracze, ze „zbyt małą próbką”, aby coś o nich powiedzieć)
– wybrać 30 graczy z największą liczbą punktów na 36 minut
– policzyć w których zespołach znajdują się tacy zawodnicy
– uszeregować zespoły według ich liczby
Na podstawie tych kryteriów przygotowujemy sobie kod:

teams = players.copy()\
               .query("positions=='center'")\
               .query("games_played>15")\
               .nlargest(30, "points_per_36")\
               .groupby("team")["points_per_36"]\
               .count()\
               .sort_values(ascending = False)

i… okazuje się, że tak przygotowany kod zwraca nam pusty zbiór. Możemy oczywiście rozbijać i analizować każdą z operacji, ale możemy wykorzystać pakiet pandas-log, który nam w tym wydatnie pomoże

!pip install pandas_log
import pandas_log

with pandas_log.enable():
    teams = players.copy()\
                   .query("positions=='center'")\
                   .query("games_played>15")\
                   .nlargest(30, "points_per_36")\
                   .groupby("team")["points_per_36"]\
                   .count()\
                   .sort_values(ascending = False)

Wywołanie przygotowanego przez nas polecenia „z” pandas_log.enable() pokaże nam co się tam właściwie stało:

1) query(expr="positions=='center'", inplace=False):
	Metadata:
	* Removed 589 rows (100.0%), 0 rows remaining.
	Execution Stats:
	* Execution time: Step Took 0.006072 seconds.

2) query(expr="games_played>15", inplace=False):
	Metadata:
	* No change in number of rows of input df.
	Execution Stats:
	* Execution time: Step Took 0.005762 seconds.
	Tips:
	* Number of rows didn't change, if you are working on the entire dataset you can remove this operation.

3) nlargest(n=30, columns="points_per_36", keep='first'):
	Metadata:
	* Picked 30 largest rows by columns (points_per_36).
	Execution Stats:
	* Execution time: Step Took 0.008734 seconds.
	Tips:
	* Number of rows didn't change, if you are working on the entire dataset you can remove this operation.

4) groupby(by="team", axis=0, level=None, as_index:bool=True, sort:bool=True, group_keys:bool=True, squeeze:bool=False, observed:bool=False):
	Metadata:
	* Grouping by team resulted in 0 groups like ,
	  and more.
	Execution Stats:
	* Execution time: Step Took 0.001026 seconds.

Jak widać, już w pierwszym poleceniu było coś nie tak (0 rows remaining). Po dokładniejszym przyjrzeniu się okazuje się, że wpisaliśmy nazwę pozycji używając małych liter.

Poprawienie tego błędu:

with pandas_log.enable():
    teams = players.copy()\
                   .query("positions=='CENTER'")\
                   .query("games_played>15")\
                   .nlargest(30, "points_per_36")\
                   .groupby("team")["points_per_36"]\
                   .count()\
                   .sort_values(ascending = False)

co w logach wygląda to już na poprawne:

1) query(expr="positions=='CENTER'", inplace=False):
	Metadata:
	* Removed 485 rows (82.34295415959252%), 104 rows remaining.
	Execution Stats:
	* Execution time: Step Took 0.007955 seconds.

2) query(expr="games_played>15", inplace=False):
	Metadata:
	* Removed 18 rows (17.307692307692307%), 86 rows remaining.
	Execution Stats:
	* Execution time: Step Took 0.007225 seconds.

3) nlargest(n=30, columns="points_per_36", keep='first'):
	Metadata:
	* Picked 30 largest rows by columns (points_per_36).
	Execution Stats:
	* Execution time: Step Took 0.006591 seconds.

4) groupby(by="team", axis=0, level=None, as_index:bool=True, sort:bool=True, group_keys:bool=True, squeeze:bool=False, observed:bool=False):
	Metadata:
	* Grouping by team resulted in 20 groups like 
		ATLANTA HAWKS,
		BOSTON CELTICS,
		CHARLOTTE HORNETS,
		DALLAS MAVERICKS,
		DENVER NUGGETS,
	  and more.
	Execution Stats:
	* Execution time: Step Took 0.001055 seconds.

Możemy więc zobaczyć upragnioną listę:

team count
0 PHOENIX SUNS 4
1 MINNESOTA TIMBERWOLVES 3
2 NEW ORLEANS PELICANS 2
3 CHARLOTTE HORNETS 2
4 LOS ANGELES CLIPPERS 2
5 MEMPHIS GRIZZLIES 2
6 WASHINGTON WIZARDS 2
7 ORLANDO MAGIC 1
8 NEW YORK KNICKS 1
9 PHILADELPHIA 76ERS 1
10 UTAH JAZZ 1
11 PORTLAND TRAIL BLAZERS 1
12 SAN ANTONIO SPURS 1
13 INDIANA PACERS 1
14 DETROIT PISTONS 1
15 DENVER NUGGETS 1
16 DALLAS MAVERICKS 1
17 TORONTO RAPTORS 1
18 BOSTON CELTICS 1
19 ATLANTA HAWKS 1

Szybka analiza eksploracyjna – pandas-profiling

TLDR: pandas-profiling to bardzo wygodne narzędzie do przeprowadzenia szybkiej analizy eksploracyjnej – obszerny raport można uzyskać za pomocą 2 linijek

Bardzo często spotykamy się po raz pierwszy z nieznanym datasetem. Chcąc podejrzeć co się w nim znajduje mamy kilka opcji. Najbardziej oczywistą wydaje się (po wczytaniu do pandasa) wykonanie na nim metody head() lub tail(). Ale to daje nam tylko pobieżną informację o kilku pierwszych (bądź ostatnich) rekordach.

Jeżeli chcemy głębiej zajrzeć w dane, z pomocą może nam przyjść pakiet pandas-profiling, który całą czarną robotę wykona za nas.

Ponieważ wielkimi krokami zbliżają się play-offy w lidze NBA możemy spróbować poczuć się jak autor profilu „O Futbolu Statystycznie” i spróbować pogrzebać w statystykach najlepszej ligi koszykarskiej. Morze statystyk można znaleźć na wspaniałej stronie basketball referrence. Tak się składa, że pewien autor stworzył swoiste pythonowe api do tej strony – pakiet basketball_reference_web_scraper .

!pip install --upgrade --quiet pandas_profiling
!pip install --quiet basketball_reference_web_scraper

import numpy as np
import pandas as pd
from pandas_profiling import ProfileReport
from basketball_reference_web_scraper import client

Po zainstalowaniu i zaimportowaniu niezbędnych pakietów można brać się do faktycznej analizy – za swój cel obierzemy graczy, którzy grali w bieżącym sezonie (2019/20). Zbiór wymaga małego oczyszczenia w polach dotyczących pozycji oraz klubu.

players = pd.DataFrame(client.players_season_totals(season_end_year=2020))
players['team'] = players['team'].astype(str).str[5:].str.replace("_", " ")
players['positions'] = players['positions'].astype(str).str.extract("'([^']*)'")

Aby móc lepiej porównywać dokonania zawodników dodamy statystyki uśrednione liczbą meczów oraz uśrednione do 36 minut.

columns = ['made_field_goals', 'attempted_field_goals', 'made_three_point_field_goals',
       'attempted_three_point_field_goals', 'made_free_throws',
       'attempted_free_throws', 'offensive_rebounds', 'defensive_rebounds',
       'assists', 'steals', 'blocks', 'turnovers', 'personal_fouls', 'points']

for stat in columns:
  players[stat+"_per_game"] = players[stat] / players["games_played"]

for stat in columns:
  players[stat+"_per_36"] = players[stat] / players["minutes_played"] * 36.

Z tak przygotowanym datasetem możemy rozpocząć naszą analizę. Utworzenie raportu jest banalnie proste – wystraczy utworzyć obiekt zawierający „podsumowanie”:

profile = ProfileReport(players, title='NBA Players Profiling Report')

A następnie wyświetlić bądź zapisać:

profile.to_widgets() # wyświetlenie w postaci osadzonego "widżetu"
profile.to_notebook_iframe() # wyświetlenie w postaci osadzonego kodu html
profile.to_file("nba_players.html") # zapis do pliku html

Raport składa się z kilku części. W pierwszej z nich znajdują się najbardziej podstawowe informacje o datasecie – liczba (i rodzaj) kolumn, liczba rekordów, informacja o rekordach pustych i zduplikowanych:

Kolejną część stanowią statystyki dotyczące poszczególnych kolumn datasetu – w przypadku zmiennych kategorycznych, dotyczą one m.in. liczby i liczności poszczególnych kategorii oraz liczby braków danych

Pozwoliło mi to np. na zauważenie, że dla zawodników, którzy zmieniali klub, występuje po kilka rekordów – po jednym dla każdego klubu w którym zagrali chociaż miutę.

W przypadku zmiennych liczbowych informacji jest jeszcze więcej – m.in. rozkład zmiennej, liczba zer, minimalne i maksymalne wartości

Dodatkowe informacje można znaleźć odsłaniając ukrytą zawartość (przycisk toggle details).

Dzięki temu można zobaczyć jak odstającym przypadkiem jest Vince Carter

Ponadto generowana jest macierz korelacji pomiędzy zmiennnymi

Co ciekawe, wiek nie jest silnie skorelowany z żadną ze zmiennych

Jednak zdecydowanie najlepszym efektem jest interaktywny wykres pozwalający na sprawdzenie interakcji pomiędzy dowolnymi dwoma zmiennymi. Dla przykładu to jak wyglądają średnie punktów i asyst w meczu

Widać bardzo dobrze outliery w osobach Jamesa Hardena, Trae Younga i LeBrona Jamesa (3 punkty w prawym górnym rogu)

Niestety – to rozwiązanie ma jednak swoje minusy – wygenerowany plik (przy dużej liczbie zmiennych) może dużo ważyć – dla tego datasetu (50 kolumn) plik z analizą w postaci html-a waży ponad 150 MB

Kod można (jak zawsze) podejrzeć na gitlabie

Prawo Goodharta (2/2)

TLDR: Kiedy miara staje się „celem”, przestaje być dobrą miarą.

Autor zdjęcia: https://www.pexels.com/@expect-best-79873

Zjawisko to można było zaobserwować historycznie (co poruszyłem w napisanej jakiś czas temu pierwszej części). Prawo to bierze nazwę od Charlesa Goodharta – brytyjskiego ekonomisty.

W oryginale brzmiało ono: „Każda obserwowana statystycznie zależność ma skłonność do zawodzenia, w momencie w którym zaczyna być wykorzystywana do celów regulacyjnych” i dotyczyła prowadzenia brytyjskiej polityki monetarnej. Goodhart zauważył, że próba wprowadzenia ograniczeń na banki komercyjne przez bank centralny nie przyniesie zamierzonego efektu, ponieważ znajdą one sposób na świadczenie danej usługi pod inną nazwą oraz w inny sposób. Aby zoptymalizować swoje zyski „skalibrują się” do wybranego kryterium.

Podobne zjawisko można zauważyć w przypadku stron internetowych. Problemem przed którym stanęli producenci wyszukiwarek było stworzenie algorytmu pozycjonowania – w Google początkowo był to PageRank (o którego dokładniejszych założeniach i implementacji można przeczytać bardzo ciekawy artykuł na Geeks for Geeks). W miarę rozwoju tych algorytmów bardzo szybko pojawiły się firmy zajmujące się pozycjonowaniem stron w internecie. Za opłatą wykorzystywały one logikę działania tych metod (które premiowały m.in. występowanie słów kluczowych oraz hiperłączy prowadzących do strony) po to, aby premiować strony w wynikach wyszukiwania. Powoduje to sytuację, w której najwyżej nie są „najlepsze” wyniki, a jedynie te „najlepiej spozycjonowane”. Z problemem nie udało się do tej pory skutecznie wygrać – przykład może stanowić próba sprawdzenia czy najbliższa niedziela jest handlowa, czy nie:

Bardzo często efekt ten można zaobserwować także w korporacjach. W momencie gdy premie są przyznawane za spełnienie danego kryterium, mogą wydarzyć się niekorzystne z punktu widzenia całej firmy, ale optymalne dla danego działu sytuacje. Dla przykładu – dział odpowiedzialny za windykację jest rozliczany jedynie za liczbę dłużników, których przekonali do spłaty. Może to doprowadzić do sytuacji, w której nie będą przypominać klientom o dacie spłaty, przez co wygenerują sobie dużo „łatwych” punktów premiowych – klientów, którzy zamierzali zapłacić, a jedynie o tym zapomnieli. Dla firmy koszt wzrośnie (dział windykacji będzie miał więcej przypadków, więc nie będzie miał czasu na częstsze napastowanie rzeczywistych dłużników), pomimo tego, że kryterium będzie wyglądać coraz lepiej.

W świecie finansowym pewnym problemem jest także kwestia „uczenia się” przez klientów tego co może być składnikiem skoringu kredytowego. W związku z tym klienci mogą podejmować próby oszukiwania na wniosku (np. zatajania rozdzielności majątkowej albo liczby dzieci na utrzymaniu), bądź nawet podejmować akcje, które mają w ich mniemaniu podnieść ich wiarygodność w oczach banku – np. brać małe niepotrzebne pożyczki, by „stworzyć dobrą historię kredytową” albo dostawać dodatkowe przelewy od znajomych, tak aby wytworzyć iluzję wyższego osiąganego dochodu. Działanie to powoduje, że dane kryterium przestaje być tak dobre, jak powinno według danych historycznych.

W przypadku bezzałogowych statków powietrznych, ważnym kryterium które dla wielu osób czy instytucji decyduje o zakupie danego produktu jest jego zasięg sterowania – to jak faktycznie daleko „dron” może odlecieć. W związku z tym niektórzy producenci stosują bardzo kierunkowe anteny – które zapewniają duży zasięg w jednym kierunku, ale „nie radzą sobie” zupełnie w innym. W skrajnych przypadkach może to powodować, że kopter będzie miał tendencję do zrywania połączenia po nawet lekkim obróceniu się. Innym przykładem z tej działki jest manipulowanie długotrwałością – czasem pozostawania w powietrzu. Można stworzyć konstrukcję, która przy bezwietrznej pogodzie, bez payloadu (akcesorium faktycznie wykonującego zadanie – np. kamery) będzie pozostawać w powietrzu bardzo długo (co zostanie skrupulatnie zaznaczone na pudełku). Ze względu na zbyt mały zapas mocy czas skróci się jednak dramatycznie, jeśli zamocujemy tam kamerę i faktycznie będziemy chcieli coś nakręcić.

Jak na złość – oba problemy.

W uczeniu maszynowym prawo to także potrafi zmaterializować się, czasami w nieoczywisty sposób. Pierwszy, najbardziej rzucający się w oczy, to kwestia przeuczania modelu. Jeżeli nasz model jest zbyt skomplikowany w stosunku do liczby posiadanych danych, może się zdarzyć, zastosowanie kryterium jakości klasyfikacji na zbiorze treningowym może skutkować jego przeuczeniem. Model „zapamięta” wtedy kombinacje – zamiast uogólnić rozwiązanie.

Czarna linia – sensowny podział. Zielona – przeuczenie.

Innym, bardziej subtelnym i trudniejszym do wychwycenia problemem jest sama kwestia definicji sukcesu i doboru próbki treningowej. Załóżmy, że pracujemy w firmie telekomunikacyjnej i chcemy stworzyć model, który będzie odrzucał klientów z wysokim prawdopodobieństwem powstania zadłużenia. Jeżeli wybierzemy jako definicję „zły klient to taki, który w ciągu roku od podpisania umowy przynajmniej raz spóźnił się z fakturą”, to model nauczony na takiej definicji może znakomicie wyglądać na papierze. Zbiór prawdopodobnie będzie lepiej zbilansowany, co może przełożyć się na jego wyniki w kryteriach jakości. Jednak ze względu na to że znakomita większość opóźnień w fakturach to krótkie opóźnienia, to model może nauczyć się cech charakteryzujących ludzi, mających skłonność do takich zachowań (np. młody wiek, korzystanie z telefonu do wykonywania opłat), a nie cech charakteryzujących „zatwardziałych dłużników”. W związku z tym możemy pozbyć się z populacji części osób, które generują zysk, a nie tych, którzy powodują straty.

Innym przykładem może być zagadnienie dość szeroko opisywane w branżowych portalach – model który miał rozróżniać wilki od psów rasy husky, pomimo tego, że dawał sobie świetnie radę na zbiorach treningowych i testowych nie działał zupełnie w rzeczywistym środowisku. Po długiej analizie wag modelu, rozwiązanie tej zagadki okazało się zaskakująco proste. Wszystkie zdjęcia wilków były wykonane w czasie jednej sesji w czasie zimy, podczas gdy zdjęcia psów pochodziły z internetu i były wykonywane w różnych porach roku i różnych miejscach. Model nauczył się zatem (z dużą dokładnością) wykrywać śnieg. (więcej na ten temat tutaj)

Reasumując – pomimo tego, że „uczenie maszynowe” brzmi bardzo górnolotnie, a metody za nim stojące są czasami wyjątkowo wysublimowane, to nadal jest to jedynie algorytm minimalizujący jakieś kryterium, bez głębszego rozumienia problemu.

Graphviz – mariaż z Pythonem (2/2)

TLDR: Z pomocą pythonowego pakietu graphviz schematy i grafy można tworzyć jeszcze prościej.

Autor zdjęcia: https://pixabay.com/users/stokpic-692575/

Jak już (mam nadzieję) udało mi się pokazać w części 1, Graphviz jest bardzo ciekawym narzędziem do tworzenia grafów i schematów z postaci czysto tekstowej.

Tworzenie wykresów może być jeszcze prostsze (i zautomatyzowane), gdyż istnieje możliwość zaprzągnięcia do pracy pythona.

Jeżeli mamy już zainstalowany sam program, musimy doinstalować pakiet graphviz w pythonie.

pip install graphviz

Po zainstalowaniu dobrze sprawdzić, czy możemy zaimportować pakiet. W 90% przypadków rozwiązanie ewentualnych problemów można znaleźć w tym wątku na stack overflow.

Przykład 1 – Wprowadzanie „Piña colada”

Podobnie jak w „samodzielnej” wersji graphviza, możemy korzystać z dwóch rodzajów schematów `Graph` – grafu nieskierowanego oraz `Digraph` – grafu skierowanego.

from graphviz import Digraph
pina_graph = Digraph(comment='Piña colada')

Powyższe polecenie tworzy pusty graf (z przypisanym komentarzem). Aby dodawać do grafu węzły i krawędzie, wykorzystywane są metody .node() oraz .edge().

pina_graph.node('letter', 'Read the letter')
pina_graph.node('response', 'Write to the author')
pina_graph.node('escape', 'Escape')
pina_graph.node('nothing', 'Do nothing')

Jak widać, w przypadku wierzchołków, podajemy identyfikator node’a (który musi być jednoznaczny) oraz jego opis (który jednoznaczny być już nie musi – dzięki temu możemy mieć na jednym wykresie dwa tak samo podpisane węzły). Możliwe jest także oczywiście dodawanie węzłów z przypisanymi atrybutami:

pina_graph.node('pina', 'Do you like piña coladas?', shape='diamond')
pina_graph.node('rain', 'Do you like getting caught in the rain?', shape='diamond')
pina_graph.node('yoga', 'Are you into yoga?', shape='diamond')
pina_graph.node('brain', 'How much brain do you have?', shape='diamond')
pina_graph.node('sex', 'Do you like making love at midnight in the dunes on the cape?', shape='diamond')
pina_graph.node('author', 'Author is the love that you\'ve looked for', shape='rectangle')

W przypadku krawędzi, podajemy identyfikator początku, końca oraz (jeżeli chcemy) opis:

pina_graph.edge('pina', 'rain', 'yes')
pina_graph.edge('rain', 'yoga', 'yes')
pina_graph.edge('yoga', 'brain', 'no')
pina_graph.edge('brain', 'sex', 'a half')
pina_graph.edge('sex', 'author', 'yes')

pina_graph.edge('pina', 'nothing', 'no')
pina_graph.edge('rain', 'nothing', 'no')
pina_graph.edge('yoga', 'nothing', 'yes')
pina_graph.edge('brain', 'nothing', 'whole')
pina_graph.edge('sex', 'nothing', 'no')

Jeżeli nie chcemy podawać opisu, możemy jednym poleceniem stworzyć kilka krawędzi, metodą edges().

pina_graph.edges([('letter', 'pina'), ('author', 'response'), ('response', 'escape')])

Na podstawie tak dodanych krawędzi można wygenerować plik dot (tak, aby móc go zapisać, bądź umieścić w README.md naszego repozytorium, z wykorzystaniem gravizo )

print(pina_graph.source) 

Co wygeneruje kod w języku dot

// Piña colada
digraph {
	letter [label="Read the letter"]
	response [label="Write to the author"]
	escape [label=Escape]
	nothing [label="Do nothing"]
	pina [label="Do you like piña coladas?" shape=diamond]
	rain [label="Do you like getting caught in the rain?" shape=diamond]
	yoga [label="Are you into yoga?" shape=diamond]
	brain [label="How much brain do you have?" shape=diamond]
	sex [label="Do you like making love at midnight in the dunes on the cape?" shape=diamond]
	author [label="Author is the love that you've looked for" shape=rectangle]
	pina -> rain [label=yes]
	rain -> yoga [label=yes]
	yoga -> brain [label=no]
	brain -> sex [label="a half"]
	sex -> author [label=yes]
	pina -> nothing [label=no]
	rain -> nothing [label=no]
	yoga -> nothing [label=yes]
	brain -> nothing [label=whole]
	sex -> nothing [label=no]
	letter -> pina
	author -> response
	response -> escape
}

Jeszcze prostsze jest wygenerowanie samego obrazka – wystarczy podać nazwę obiektu.

pina_graph

Przykład 2 – Generowanie automatycznego wykresu

Dzięki temu, że wszystko jest ładnie opakowane w pythonowy interfejs, możliwe jest automatyzowanie tworzenia wykresu i wykorzystanie go do wizualizacji przepływu procesu.

Tutaj mała próbka takiego zastosowania – stworzenie prostego wykresu, pokazującego liczby podzielne przez daną liczbę.

number_graph = Digraph(comment='Dzielniki')
for i in range(1, 13):
    number_graph.node(str(i))
    for j in range(1, 13):
        if j%i == 0:
            number_graph.edge(str(i), str(j))

number_graph

Wywołanie tego kodu poskutkuje otrzymaniem takiego obrazka:

Więcej o pakiecie i jego możliwościach można poczytać w jego dokumentacji. Natomiast kod do obu przykładów dostępny jest na githubie

Wybory w 100 okręgach? Analiza

TLDR: Zastosowanie mniejszych okręgów wyborczych wzmocniłoby duże ugrupowania. Przy tej samej liczbie głosów PiS zyskałby 41 posłów.

Niedawno w przestrzeni publicznej (głównie na twitterze) pojawiła się propozycja zastąpienia obecnych okręgów do Sejmu mniejszymi tak, aby „zbliżyć posłów do wyborców”.

Bez wchodzenia w niepotrzebne spory polityczne postanowiłem sprawdzić, jakie byłyby wyniki takiego eksperymentu. Oparłem się na wynikach do wyborów do sejmu w 2019, dostępnych na stronie PKW: https://sejmsenat2019.pkw.gov.pl/sejmsenat2019/pl/dane_w_arkuszach
Przyjęte przeze mnie (dość mocne) założenia:

  • Okręgi do Sejmu i Senatu miałyby ten sam kształt
  • Partie nie dokonałyby ruchów koalicyjnych
  • Wyniki wyborów w poszczególnych komisjach nie zmieniłyby się pomimo zmiany kształtów okręgów wyborczych
  • Mandaty rozkładałyby się proporcjonalnie do liczby mieszkańców
  • Do podziału mandatów w okręgach nadal wykorzystywanoby metodę d’Hondta

Rezultaty wyglądałyby następująco:

PiSKOLewicaPSLKonfederacjaMN
Wyniki przy 100 okręgach276144291001
Rzeczywiste wyniki2351344930111
Różnica+41+10-20-20-110

Wnioski jakie można wyciągnąć z tej symulacji:

  • zastosowanie małych okręgów wyborczych premiuje silnie duże ugrupowania – PiS przy dokładnie takiej samej ilości głosów miałby większość 3/5 głosów w Sejmie
  • najbardziej „karane” są małe partie reprezentowane w miarę równomiernie we wszystkich okręgach (PSL, Konfederacja)
  • efektywny próg wyborczy może być znacznie wyższy niż 5% – Konfederacja pomimo uzyskania 6,81% głosów nie wprowadziłaby do sejmu żadnego posła
  • rozwiązanie to prawdopodobnie doprowadziłoby do konieczności utworzenia bloku partii opozycyjnych i do efektywnego wytworzenia się systemu dwupartyjnego

Na githubie dostępny jest kod analizy, a także dokładniejsze wyniki w każdym z okręgów

Pandas – szybsza alternatywa dla apply

TLDR: np.vectorize() jest nawet kilkaset razy szybszy od pd.apply()

Czasami podczas pracy z pandasowym DataFrame’m zachodzi potrzeba wykonania bardziej nietrywialnej operacji na zbiorze. Załóżmy, że analizujemy ceny nieruchomości w Kalifornii, korzystając z popularnego zbioru California Housing Prices.

import pandas as pd
import numpy as np

california_housing = pd.read_csv("./sample_data/california_housing_train.csv")

Nasz przełożony podpowiada nam, że cena domów, w których sypialnie zajmują dużą część budynku powinny być tańsze niż wskazuje na to mediana wartości budynku. W związku z tym, chcemy zmniejszyć cenę dla takich budynków, natomiast zwiększyć dla budynków, w których taka sytuacja nie występuje.

%%timeit
california_housing['median_house_value_scaled_1'] = \
    california_housing.apply(lambda x: x["median_house_value"]\
                             if x["total_rooms"]> 3. * x["total_bedrooms"]\
                             else 1.5 * x["median_house_value"], axis = 1)

Oczywiste wydaje się być użycie apply(). Tak napisany kod zajmuje Google Colabowi 594 ms. Nie wydaje się być to tragedią. Dataset ma jednak jedynie 17 000 rekordów. Załóżmy, że nasz dataset jest jednak 100 razy większy:

california_housing_long = pd.concat([california_housing]*100)

Wówczas wykonanie tej samej operacji zajmuje już Colabowi ponad minutę. Zakładając, że mamy takich operacji do wykonania kilka (albo, nie daj Boże, musimy to robić regularnie) jest to zauważalna strata czasu.

Z pomocą przychodzi nam np.vectorize(). Dzięki jego zastosowaniu, możemy zachować przejrzysty kod, który będzie liczył się o wiele szybciej – kod zapewniający tą samą funkcjonalność, dla zbioru o 1 700 000 rekordów liczy się 481 ms – ponad 130 razy szybciej niż analogiczny kod z wykorzystaniem apply()

def house_value_scale(median_house_value, total_rooms, total_bedrooms):
  if total_rooms > 5. * total_bedrooms:
    return median_house_value
  else:
    return median_house_value * 1.5

%%timeit
california_housing_long['median_house_value_scaled_2'] = np.vectorize(house_value_scale)\
    (california_housing_long['median_house_value'], 
     california_housing_long['total_rooms'], 
     california_housing_long['median_house_value'])

Korzystanie z niej wygląda nieco inaczej niż w przypadku apply(). Najlepiej najpierw zdefiniować funkcję operującą na pojedynczym rzędzie (bądź kolumnie).

Vectorize tworzy z niej funkcję operującą już na „kolumnach” – jako argumenty do utworzonej funkcji (druga para nawiasów) podajemy „kolumny”.

Wideosegment (1/2019)

Z małym poślizgiem, ale zgodnie z zapowiedzią, weekendowe 3 filmy:

DJI Film School – Świetne filmy przekazujące masę informacji na temat filmowania z użyciem dronów i gimbali. Oczywiście jest w nich trochę autopromocji, ale większość porad jest bardzo profesjonalna.

Projekt Selfie – rozwijany przez członków Koła Naukowego Robotyków ciekawy projekt autonomicznego samochodu w skali 1:10.

Doskonała symulacja działania sieci neuronowej (MLP) zajmującej się rozpoznawaniej obrazów.

Amazon otwiera swoje zasoby materiałów z ML za darmo!

Chyba najlepszy przedwczesny prezent mikołajkowy dla wszystkich entuzjastów ML. Amazon udostępnia swoje kursy z zakresu Analizy Danych i Uczenia.

https://aws.amazon.com/training/learning-paths/machine-learning/

Dostępne są 4 ścieżki:
* Developer
* Business Decision Maker
* Data Scientist
* Data Platform Engineer

Na ich głębszą analizę przyjdzie jeszcze czas, ale na pierwszy rzut oka wygląda to bardzo, bardzo zachęcająco.

W razie gdybyście mieli problemy z uruchomieniem, wyłączcie dla tej strony adblocka i blokadę Pop-upów.

Kursy MOOC – cz. 1 (Data Science)

Kursy MOOC (Massive Open Online Courses) są jedną z najlepszych rzeczy jakie spotkały Internet. Pozwalają bezpłatnie (albo stosunkowo tanio) nabyć umiejętności w przystępny sposób. Postaram się przybliżyć te kursy, przez które miałem okazję przebrnąć.

W mojej przygodzie miałem przyjemność bycia uczestnikiem kursów głównie na 2 platformach: coursera oraz edX. Poza pojedynczymi kursami z jakiegoś zagadnienia oferują one grupy kursów obejmujące jakiś szerszy zakres (w courserze nazywa się to Specialization  w edX – MicroMasters).

Na pierwszy ogień idą 2 specjalizacje z Data Science

Data Science Specialization – [link]

Nie mógłbym nie zacząć od specjalizacji, od której w zasadzie zaczęła się moja poważna przygoda z DS.

Składa się z 10 kursów i obejmuje bardzo duży zakres materiału (proces pozyskiwania danych, statystka, regresja). Jest prowadzona w R, na szczególne polecenie zasługują R Programming oraz Getting and Cleaning Data – stanowią bardzo dobre wprowadzenie do R. Jest bardzo dobrze (miejscami wręcz łopatologicznie) prowadzona, ale wymaga dużo pracy  samodzielnej. Do większości kursów są opracowane bardzo dobre materiały, które w formie papierowej często służą mi do dziś – choćby ta książka [pdf].

Jej dużym minusem jest to, że jej tempo nie jest ustabilizowane – momentami strasznie się dłuży, . Pierwszy kurs dla osób mających jakąkolwiek styczność z IT jest zbędny. Koło 7-8 kursu zaczyna się mieć wrażenie przeciągniętego. Minusem było (przynajmniej w zeszłych edycjach) zastosowanie Peer Review (oceny prac domowych przez innych studentów) – mechanizm nie wydawał się dopracowany, wyglądało to tak, jakby niektórzy użytkownicy nawet nie odpalali u siebie kodów – ale robiłem je parę lat temu i pewnie do tego czasu już to znacznie poprawili.

Moja ocena: Z perspektywy czasu mam do niej bardzo duży szacunek. Jest bardzo dobra dla osób, które mają sporo czasu i chcą dobrze zgłębić tajniki analizy danych w R, ale raczej nie tyle pod kątem biznesowym co zastosowań naukowych.

Applied Data Science with Python Specialization – [link]

Ta specjalizacja zajmuje się  przetwarzaniem danych w języku Python.

Jej dużym plusem jest to, że nie jest tak przegadana jak wyżej wspomniana. Zawiera 5 kursów (python w Data Science, reprezentacja danych, Machine Learning, Text Mining, analiza sieci społecznościowych)  i sprawia wrażenie dobrze rozplanowanej. Jej bardzo dużym plusem są wplatane tu i ówdzie ciekawostki – to tam poznałem kanał Data Skeptic – dataskeptic.com

Bardzo dobre wprowadzenie do pythona w DS stanowi pierwszy kurs – prowadzący skupia się na bibliotece pandas i na tym co jest w niej najważniejsze. Bardzo dużym plusem jest też forma prowadzenia kursu – dostępne są arkusze jupyterowe, dzięki czemu można robić ćwiczenia nawet będąc na uczelnianym bądź „pracowym” komputerze.

Małym minusem (dla mnie) jest drugi kurs (reprezentacja danych) – jest on nieco mniej merytoryczny i w dużej części zajmuje się samą otoczką reprezentacji danych niż konkretnymi przykładami. Także zagadnienia Machine Learningu są potraktowane nieco po macoszemu.