Skąd i dokąd. Krótka acz treściwa przygoda z wizualizacją danych.

Ponieważ nie samym SQL-em człowiek żyje...

Chociaż czasem ubolewam, że ludzie mówią tyloma różnymi językami. Jakby tak wszyscy się przestawili na SQL, byłoby dużo prościej.

... żyje, powiadam, spróbowałem ostatnio pobawić się w wizualizację...

A może "we wizualizację"? Skoro jest "we wtorek"? No ale z drugiej strony jednak "w środę" i "w Wiedniu" więc zostaje.

... danych za pomocą Pythona. Normalnie by mi się nie chciało, ale akurat (A) miałem chwilę, bo wszystkie zadania służbowe czekały na jakąś reakcję z drugiej (a czasem i trzeciej) strony, tudzież (B) nawet jeżeli mi się nie chce na stare lata klepać kodu, to mamy teraz LLM-y, za pomocą których można masowo i względnie bezwysiłkowo testować różne jednorazowe pomysły i sprawdzać co z tego wyniknie.

Akurat dostałem automatycznego maila od Numbeo, z linkiem do danych zgromadzonych przez ostatnie dwa lata. Można się stamtąd dowiedzieć mieszkańcy których krajów najczęściej wyszukują jakich innych krajów w serwisie.

Czyli w drugiej pochodnej, do jakich krajów najchętniej chcieliby się przeprowadzić, bo przyszli emigranci to największa grupa tamecznych użytkowników.

Dla każdego kraju "źródłowego" mamy wymienionych 20 najczęściej wyszukiwanych krajów "docelowych", wraz z procentami. Tutaj link.

Dane są wprawdzie opublikowane w formie tekstowej, "do czytania", ale przerobienie tego na proste, trzykolumnowe CSV zajęło mi z pomocą LLM mniej więcej tyle czasu, co napisanie tego akapitu:

import csv
import re

input_file = "destinations.txt"
output_file = "destinations.csv"

data = []

with open(input_file, "r", encoding="utf-8") as file:
    source_country = None
    for line in file:
        line = line.strip()

        match = re.match(r"(.+?)'s Top Destination Searches", line)
        if match:
            source_country = match.group(1)
            continue

        if not line:
            continue

        if source_country:
            destination_data = line.split(" - ")
            if len(destination_data) == 2:
                destination_country = destination_data[0].strip()
                percentage = destination_data[1].replace("%", "").strip()
                data.append([source_country, destination_country, percentage])

with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
    csv_writer = csv.writer(csvfile)
    csv_writer.writerow(["source_country", "destination_country", "percentage"])
    csv_writer.writerows(data)

Jeżeli chcesz spróbować, ściągnij sobie do pliku destinations.txt dane z powyższego artykułu, począwszy od linijki "Albania's Top Destination Searches" aż do ostatniej, dwudziestej pozycji w bloku "Vietnam's Top Destination Searches". Plik powinien mieć dokładnie 2169 linii.

Wykonanie powyższego kodu wypluje nam plik destinations.csv, z trzema kolumnami: source_country, destination_country, percentage. Od tej pory będziemy używać tego właśnie pliku do dalszych eksperymentów.

Jako wielki fan grafów postanowiłem zacząć od GraphViz, technologii, której wiek liczy się już w interglacjałach. A więc kolejny skrypt przekształcający nasze csv na dot.

import csv

input_csv = "destinations.csv"
output_dot = "destinations.dot"

edges = []

with open(input_csv, 'r', encoding='utf-8') as csvfile:
    csv_reader = csv.reader(csvfile)
    next(csv_reader)
    for row in csv_reader:
        source_country, destination_country, percentage = row
        edges.append((source_country, destination_country, percentage))

with open(output_dot, 'w', encoding='utf-8') as dotfile:
    dotfile.write("digraph CountrySearches {\n")
    dotfile.write("    rankdir=LR;\n")  # Optional: sets layout from left to right
    dotfile.write("    node [shape=ellipse];\n\n")

    for source, destination, percentage in edges:
        dotfile.write(f'    "{source}" -> "{destination}" [label="{percentage}%"];\n')

    dotfile.write("}\n")

Pomysł zacny. Wykonanie... no cóż. Pacjent przeżył, ale co to za życie. Wyrenderowałem ten plik za pomocą fdp, sfdp, twopi, neato... Efekty poniżej:

FDP

SFDP

TWOPI

NEATO

Na koniec spróbowałem jeszcze domyślnego DOT, zaczął mielić i zrobiła się pora lunchu, zostawiłem więc otwartą konsolę i udałem się na posiłek.

Po powrocie dalej sie mieliło, zostawiłem w tle - w końcu po godzinie i czterdziestu minutach (!) wypluł plik png o rozmiarze ciut ponad 100 MB. Nie będę tu wrzucał oryginału, bo mi szkoda miejsca na serwerze z blogiem. Poniżej zoom 3%:

Oczywiście przy zbliżeniu 100% wszystko jest ładnie czytelne:

No ale tak to se można. Żeby to miało jakiś sens, potrzebowałbym monitora o przekątnej co najmniej trzy metry oraz rozdzielczości ponad 25K pikseli w każdą stronę. Nidyrydy.

No dobra. Skoro GraphViz nie podołał, trzeba odwiesić aureolę na gwóźdź i zaprzyjanić się z jakimiś bardziejszymi cosiami. Żeby nie czuć się jak zdrajca (GraphViz to technologia, z którą znamy się jak łyse konie od bardzo dawna), spróbowałem jeszcze ograniczyć listę krajów docelowych do trzech najbardziej popoularnych dla każdego kraju. Tu już nie musiałem czekać zbyt długo i po paru sekundach dostałem plik o rozdzielczości 2026 x 6757 px. Nadal za duży do celów praktycznych, ale przynajmniej coś już trochę widać.

Całość: zoom 14%

Fragment, zoom 48%:

Odłożywszy GraphViz na stronę zacząłem się bawić wizualizacjami wbudowanymi w samego Pythona. Czyli Matplotlib, Seahorse i różne inne, podobne.

Zacząłem od mapy cieplnej:

Niby fajna, ale jakoś mało informacjonośna. Acz zdecydowanie lepsza od bazgrołów z smnego początku wpisu 🙂

Potem spróbowałem słupków, ale wynik - chociaż w miarę czytelny - dupy nie urywa:

Potem całkiem niechcący udało mi się wyprodukować takie coś:

Wygląda może i ładnie, ale pożytku z tego nie ma żadnego. Przerzuciłem się więc na bąbelki:

Gdyby nie zbyt gęsto upakowane etykiety na osi pionowej, może mielibyśmy zwycięzcę. Chociaż nie, bo kolor bąbelka powinien w teorii pokazywać coś innego niż jego rozmiar, a tutaj jedno z drugim idzie w parze.

Bo praktyka zgadza się z teorią tylko teoretycznie, bo w praktyce to już niekoniecznie.

Na chwilę opuściłem świat Matplotlib i przerzuciłem się na Plotly, z takim oto efektem:

Ładne? Ładne. Pożyteczne? Hmmm.

Kolejny eksperyment polegał na zrobieniu osobnego wykresu dla każdego kraju docelowego a nastęnie poukładaniu tych wszystkich wykresów obok siebie. Efekt jest godny pożałowania:

Za gęsto. Może trzeba się ograniczyć tylko do pięciu najpopularniejszych krajów docelowych?

Paskudztwo. Ale poszedłem za tą myślą i zrobiłem prosty wykres słupkowy pokazujący trzy najpopularniejsze destynacje dla każdego kraju:

Ładne, czytelne, użyteczność trochę kuleje. Trzeba każdy kraj poskładać do jednego słupka:

Zaczyna to nabierać jakiegoś sensu. Jeszcze posortować po wysokości słupka...

A może zamiast top 3 zrobić top 5?

I tutaj skończył mi się rozpęd. Spróbowałem jeszcze na koniec zrobić sobie zestawienie które kraje są najpopularniejsze pod względem liczby krajów źródłowych (z ponimięciem procentów). Wyszło mi takie coś:

Polska była wyszukiwana przez obywateli 46 różnych krajów. Ha.

Potem jeszcze przez moment spróbowałem zrobić mapę cieplną z płynnymi gradientami (czyli żeby nie było widać linii między wierszami i kolumnami), ale poległem i zakończyłem ćwiczenie.

Jestem ciekaw co jeszcze można by tutaj spróbować zrobić, żeby to ładnie zwizualizować.

Jakieś pomysły?

Hmmm.

Arranging connections…

Dodaj komentarz

Komentarze mile widziane.

Jeżeli chcesz do komentarza wstawić kod, użyj składni:
[code]
tutaj wstaw swój kod
[/code]

Jeżeli zrobisz literówkę lub zmienisz zdanie, możesz edytować komentarz po jego zatwierdzeniu.