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

https://xpil.eu/JCZAi

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.

https://xpil.eu/JCZAi

Leave a Comment

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.