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

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

Graphviz – diagramy i schematy (1/2)

TLDR: Graphviz to program pozwalający na tworzenie schematów na podstawie komend w dedykowanym języku: dot.

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

Tworzenie schematów bywa trudnym zajęciem. By zostało dobrze zrobione, wymaga od autora umiejętności graficznych. Dodanie nowych elementów bardzo często wiąże się z koniecznością zmiany układu całego schematu. Dodatkowo, wersjonowanie takiego pliku bywa utrudnione – zwłaszcza jeżeli jest on zapisany w formacie graficznym trudno wychwycić różnicę pomiędzy kolejnymi iteracjami pliku.

Graphviz pozwala wyeliminować obie te słabości i wygenerować estetyczne diagramy, na podstawie „kodu źródłowego”, w języku dot. Aby wypróbować jego możliwości, można go oczywiście zainstalować, ale można też wykorzystać jedno z dostępnych API, np to na witrynie GitHuba.

Załóżmy, że chcemy stworzyć wykres, obrazujący przebieg zwrotek piosenki Hey Jude Beatelsów:

Kod źródłowy dla takiego wykresu będzie wyglądał następująco:

digraph G {
  node [shape = box];
  
  /*1 zwrotka*/
  start -> negation -> worsening -> song_improvement -> 
    remembering -> permission -> indication -> 
    start_permission -> improvement;
  
  improvement -> better;
  waaaaaa -> na;

  
  /*2 zwrotka*/
  negation -> fear -> conquering ->  remembering -> 
  injection -> indication -> origin -> improvement;
  
  /*3 zwrotka*/
  negation -> disappointment -> retrieval -> remembering;

  start [label = "Hey Jude"];
  negation [label = "don't" shape = octagon];
  worsening [label = "make it bad"]
  song_improvement [label = "take a sad song and make it better"]
  remembering [label = "remember to"]
  permission [label = "let her into your heart"]
  indication [label = "then you"]
  start_permission[label ="can start"]
  improvement[label = "to make it better"]
  
  fear[label = "be afraid"]
  conquering[label = "you were made to go out and get her"]
  injection[label = "let her under your skin"]
  origin[label = "begin"]
  
  disappointment[label = "let me down"]
  retrieval[label = "you have found her, now go and get her"]
  
  subgraph cluster_na {
    color=lightgrey;
    na -> na;
    label = "na";
  }

  subgraph cluster_better {
    color=lightgrey;
    better -> better -> waaaaaa;
    label = "better";
  }
}

Dostępne są dwa rodzaje grafów – skierowane digraph oraz nieskierowane graph.

Jak można zaobserwować, diagram składa się z 3 głównych elementów:
– węzłów (node)
– krawędzi (egde)
– podgrafów (subgraph)
Każdemu z nich mogą zostać przypisane atrybuty. Do najważniejszych z nich należy atrybut label – odpowiada on za to, jak będzie podpisany dany węzeł bądź krawędź. Można również ustawić tam kolor, (color), kształt (shape) bądź wypełnienie. Ich pełna lista jest dostępna tutaj.

Polączenia pomiędzy węzłami tworzy się poprzez zastosowanie łącznika ->, jeżeli chce się pokazać kierunek, bądź — jeżeli chce się pokazać jedynie połączenie.

Komentarze można dodawać w takim samym stylu jak w C/C++ – jednolinijkowe porzez //, wielolinijkowe przez /* */.