Wszystkie artykuły

KajoData / Python

Podstawy Pythona: od zmiennej do sprawdzonego raportu w pandas

Przechodzę przez jeden kompletny przykład: wczytanie zamówień, sprawdzenie danych i raport, którego wynik potrafisz wyjaśnić.

Kajo RudzińskiOpublikowano Aktualizacja 11 min czytania

Dwie przezroczyste komory porządkują geometryczne bloki, a lupa leży przy gotowym wyniku.

Podstaw Pythona najłatwiej uczę się na zadaniu, w którym wiadomo, jaki wynik powinien wyjść. Sama lista typów i funkcji szybko zmienia się w katalog słówek. Dlatego przejdę przez mały raport: ile opłaconych zamówień mamy w każdym kanale i jaka jest suma znanych kwot. Po drodze pokażę zmienne, listy, słowniki, funkcję oraz podstawowe operacje pandas.

Dane będą syntetyczne i celowo trochę niewygodne. Pojawi się kwota równa zero, brakująca kwota i zamówienie anulowane. Dzięki temu zobaczysz, dlaczego działający kod wymaga jeszcze sprawdzenia znaczenia wyniku. Nie potrzebujesz znać całej biblioteki przed rozpoczęciem. Potrzebujesz rozumieć kolejne kroki i umieć wrócić do rekordu, który wyjaśnia liczbę w raporcie.

Przykłady sprawdziłem w Pythonie 3.14.7 i pandas 3.0.6. Bloki z kodem Python wykonuj kolejno w jednym pliku albo świeżej sesji. Polecenia instalacyjne należą do terminala. Nie wpisuj ich do pliku .py. Ta drobna różnica potrafi zatrzymać pierwsze ćwiczenie jeszcze przed spotkaniem z danymi.

Przygotuj małe, osobne środowisko

Zacznij od pustego folderu na ćwiczenie. Zakładam, że polecenie python uruchamia zainstalowany interpreter. Sprawdź jego wersję, zanim zaczniesz instalować pakiety. Jeśli polecenie nie jest rozpoznawane, najpierw popraw instalację lub wybór interpretera w edytorze. Losowe powtarzanie instalacji pandas w kilku terminalach zwykle zwiększa liczbę środowisk, a nie pewność, którego używasz.

Poniższe polecenia są przeznaczone dla PowerShella w Windows. Wywołuję interpreter środowiska bezpośrednio, więc nie trzeba zmieniać zasad uruchamiania skryptów ani aktywować środowiska. Dokumentacja venv wyjaśnia izolację zależności. Na macOS i Linuksie interpreter środowiska znajduje się zwykle w katalogu bin, a nie Scripts.

python --version
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install pandas==3.0.6
.\.venv\Scripts\python.exe analysis.py

Plik analysis.py utwórz w folderze ćwiczenia i umieść w nim kolejne bloki. Przypięta wersja pandas pozwala odtworzyć środowisko tego tekstu; nie oznacza, że każdemu projektowi należy narzucić tę wersję. Anaconda również może być użyteczna, zwłaszcza w istniejącym środowisku zespołu. Na potrzeby tego małego zadania wystarcza Python z osobnym środowiskiem i jedną biblioteką.

Zmienna nadaje nazwę wartości

Zmienna pozwala odwołać się do obiektu po nazwie. W Pythonie nie deklarujemy wcześniej osobnej szufladki na konkretny typ. Przypisanie wiąże nazwę z wartością, a obiekt ma swój typ. W praktyce warto wybierać nazwy mówiące, co reprezentują dane. amount_cents jest bardziej pomocne niż x, szczególnie gdy później wracasz do obliczeń.

# example: values
order_id = "001"
amount_cents = 12000
is_paid = True
missing_amount = None
print(order_id, amount_cents / 100, is_paid, missing_amount)

Wynik to 001 120.0 True None. Identyfikator przechowuję jako tekst, żeby zachować początkowe zera. Kwotę zapisuję jako całkowitą liczbę groszy, dzięki czemu samo sumowanie w przykładzie nie wymaga reprezentowania ułamków dziesiętnych. Wartość logiczna opisuje warunek, a None sygnalizuje brak wartości. To różne znaczenia, nawet jeśli w arkuszu wszystkie mieszczą się w jednej komórce.

Nie traktuj None, zera i pustego tekstu jako zamienników. Zero może być poprawną kwotą. Brak kwoty oznacza, że jej nie znamy. Pusty tekst może wynikać ze sposobu eksportu. Decyzję o interpretacji podejmujemy na podstawie danych i procesu, a potem zapisujemy ją w kodzie. Więcej podstaw znajdziesz w artykule o zmiennych, typach i operatorach.

Lista, słownik i krotka porządkują informacje

Lista przechowuje uporządkowaną kolekcję elementów. Słownik wiąże klucz z wartością, więc dobrze nadaje się do małego rekordu. Krotka może reprezentować stały zestaw elementów, na przykład oczekiwane nazwy kolumn. Dokumentacja struktur danych Pythona opisuje ich zachowanie dokładniej. Tutaj potrzebujemy jedynie zobaczyć, jak te obiekty pomagają nazwać dane.

# example: structures
orders = [
    {"order_id": "001", "channel": "web", "amount_cents": 12000},
    {"order_id": "002", "channel": "store", "amount_cents": 0},
]
columns = ("order_id", "channel", "amount_cents")
print(orders[0]["order_id"])
print(sum(order["amount_cents"] for order in orders))

Otrzymasz 001 oraz 12000. Indeks pierwszego elementu listy wynosi zero. Odwołanie przez klucz słownika pozwala pobrać konkretną informację. Wyrażenie w sum przechodzi po rekordach i wybiera kwotę. Jeśli klucz nie istnieje, dostaniesz błąd, który warto zrozumieć. Automatyczne zastąpienie wszystkiego zerem mogłoby ukryć problem ze strukturą danych.

Krotka nie pozwala podmienić swoich elementów, ale może zawierać obiekty, które same są modyfikowalne. Nie trzeba teraz budować całej teorii mutowalności. Wystarczy unikać uproszczenia, że każdy obiekt umieszczony w krotce staje się całkowicie niezmienny. Przy pierwszych zadaniach skup się na czytelnych danych i świadomym wyborze struktury.

Przygotuj kompletne dane wejściowe

Nasz raport użyje sześciu zamówień. Każdy wiersz ma być jednym zamówieniem, a identyfikator ma być unikalny. Kanały to web, store i partner. Status paid oznacza, że rekord wchodzi do raportu, a cancelled go wyklucza. Te zasady obowiązują w ćwiczeniu. W prawdziwym projekcie trzeba je uzgodnić z osobą odpowiedzialną za proces.

# example: input
from io import StringIO
import pandas as pd

csv_text = """order_id,channel,status,amount_cents
001,web,paid,12000
002,store,paid,0
003,web,cancelled,9000
004,store,paid,
005,web,paid,8000
006,partner,paid,
"""
df = pd.read_csv(
    StringIO(csv_text),
    dtype={"order_id": "string", "channel": "string",
           "status": "string", "amount_cents": "Int64"},
)
print(df.shape)

Wynik (6, 4) oznacza sześć wierszy i cztery kolumny. StringIO pozwala potraktować tekst jak plik, więc nie musisz pobierać dodatkowego zbioru. Później możesz przekazać do read_csv ścieżkę do własnego pliku. Najpierw jednak uruchom dokładnie te dane, żeby oddzielić naukę kodu od problemów z formatem nowego eksportu.

Typ Int64 z wielkiej litery jest typem pandas obsługującym liczby całkowite i braki. Dzięki jawnym typom identyfikator pozostaje tekstem. Dokumentacja read_csv opisuje też separator, kodowanie i rozpoznawanie braków. Domyślne reguły mogą uznać niektóre napisy, na przykład NA, za brak. Dla własnego źródła sprawdź, czy taki napis nie jest prawidłowym kodem.

Zanim policzysz, obejrzyj tabelę

DataFrame przypomina tabelę z nazwanymi kolumnami i indeksem wierszy. Samo utworzenie tabeli nie sprawdza wszystkich reguł biznesowych. Może poprawnie wczytać rekordy, które są powtórzone, mają nieoczekiwany status albo pochodzą z innego okresu niż zakładaliśmy. Dlatego przed grupowaniem warto obejrzeć rozmiar, typy i kilka pełnych rekordów.

# example: inspect
print(df.head(3).to_string(index=False))
print(df.isna().sum().to_dict())
print(df["order_id"].duplicated().sum())

W naszym zbiorze brakuje dwóch kwot, a pozostałe kolumny są kompletne. Powtórzonych identyfikatorów jest zero. Pierwsze trzy wiersze pomagają sprawdzić format, lecz nie dowodzą jakości całej tabeli. To właśnie dlatego osobno liczę braki i duplikaty. Dobry podgląd próbki oraz kontrola wszystkich rekordów odpowiadają na inne pytania.

Gdy kontrola wykryje powtórkę, nie usuwaj jej automatycznie. Być może źródło zawiera pozycje zamówienia, a twoje założenie o jednym wierszu na zamówienie było błędne. W naszym ćwiczeniu duplikat jest powodem do zatrzymania raportu. W innym zbiorze może być poprawną częścią modelu. Kod powinien realizować ustaloną regułę, zamiast zgadywać znaczenie rekordów.

Funkcja opisuje powtarzalną operację

Funkcja przyjmuje dane, wykonuje określoną pracę i zwraca wynik. Zamiast kopiować kilka filtrów w różne miejsca, zapiszę raport w jednej funkcji. Najpierw sprawdzę wymagane kolumny i podstawowe założenia. Dopiero później wybiorę opłacone zamówienia. To pozwala zobaczyć, gdzie kończy się kontrola danych, a zaczyna właściwe obliczenie.

# example: report
def paid_by_channel(data):
    required = {"order_id", "channel", "status", "amount_cents"}
    if not required.issubset(data.columns):
        raise ValueError("Missing required columns")
    if data[["order_id", "channel", "status"]].isna().any().any():
        raise ValueError("Missing identifier, channel or status")
    if data["order_id"].duplicated().any():
        raise ValueError("Duplicate order identifiers")
    if not data["status"].isin(["paid", "cancelled"]).all():
        raise ValueError("Unexpected status")
    if (data["amount_cents"].dropna() < 0).any():
        raise ValueError("Negative amount in this exercise")
    paid = data.loc[data["status"].eq("paid")]
    return paid.groupby("channel", sort=True).agg(
        orders=("order_id", "size"),
        known_amounts=("amount_cents", "count"),
        known_amount_cents=("amount_cents",
                            lambda values: values.sum(min_count=1)),
    )

report = paid_by_channel(df)
print(report.to_string())

Wcięcia należą do składni Pythona. Instrukcja raise przerywa wykonanie, gdy założenie nie jest spełnione. return oddaje tabelę wynikową do miejsca wywołania. Nazwy kolumn w raporcie są celowo opisowe. Suma znanych kwot nie zawsze jest pełną wartością opłaconych zamówień, jeżeli część kwot jest nieobecna.

Ta funkcja zakłada typy przygotowane przez pokazane wczytanie CSV. Nie jest uniwersalnym walidatorem dowolnego pliku. Nie sprawdza choćby spacji w identyfikatorze ani wszystkich możliwych reguł kwot. Ujemne wartości odrzucam dlatego, że przykład nie modeluje zwrotów. W projekcie obejmującym zwroty taka kontrola wymagałaby innej definicji.

Filtrowanie i grupowanie bez zgadywania

Warunek data["status"].eq("paid") tworzy serię wartości logicznych. loc wybiera wiersze spełniające warunek. Następnie groupby dzieli je według kanału, a agg opisuje miary dla każdej grupy. Nie musimy pisać osobnej pętli dla sklepu i internetu. Nowa poprawna kategoria może pojawić się w wyniku jako kolejna grupa.

W dokumentacji groupby znajdziesz opcje dotyczące sortowania i brakujących kluczy. Tutaj sortowanie jest jawne, a brak kanału odrzuca wcześniejsza kontrola. Dzięki temu nie zgubimy po cichu rekordu bez kategorii. W oryginalnej wersji artykułu przykład grupował po city, choć pokazana tabela nie miała takiej kolumny. Teraz cały przebieg używa jednego kompletnego zbioru.

size liczy wiersze, a count w wybranej kolumnie liczy wartości niebrakujące. Różnica jest ważna dla kanału store: mamy dwa zamówienia, ale tylko jedną znaną kwotę. Suma z min_count=1 pozostawia brak, gdy grupa nie ma żadnej znanej kwoty. Bez takiej decyzji łatwo pomylić nieznany wynik z rzeczywistym zerem.

Osobne przegródki symbolizują brak wartości, zero i dwa podobne rekordy.

Odczytaj wynik razem z ograniczeniem

Kanał Zamówienia Znane kwoty Suma (gr)
partner 1 0 brak
store 2 1 0
web 2 2 20000

Kanał web ma dwa opłacone zamówienia z kompletnymi kwotami: 12000 i 8000 groszy. Anulowane zamówienie z kwotą 9000 nie weszło do wyniku. Dla store znamy jedną kwotę równą zero, a drugiej nie znamy. Dla partner nie znamy żadnej. Nie wyciągałbym z tej tabeli wniosku, że partner niczego nie sprzedał.

Łącznie raport obejmuje pięć opłaconych zamówień, trzy znane kwoty i sumę znanych kwot równą 20000 groszy. To 200 jednostek waluty w naszym przykładzie. Nie jest to udowodniona pełna wartość wszystkich pięciu zamówień. Gdy przekazujesz wynik dalej, informacja o dwóch brakach powinna podróżować razem z sumą, a nie zostać w twoim notatniku.

Sprawdź wynik prostymi asercjami

Asercja wyraża oczekiwanie w ćwiczeniu i zgłasza błąd, jeśli jest fałszywe. Nie używam jej zamiast jawnej walidacji w funkcji, bo Python może wyłączyć asercje w trybie optymalizacji. Tutaj służy jako krótka kontrola znanego przykładu. W produkcyjnym rozwiązaniu sposób raportowania błędów trzeba dobrać do procesu.

# example: checks
assert report["orders"].sum() == 5
assert report["known_amounts"].sum() == 3
assert report.loc["web", "known_amount_cents"] == 20000
assert report.loc["store", "known_amount_cents"] == 0
assert pd.isna(report.loc["partner", "known_amount_cents"])
assert paid_by_channel(df.iloc[:0]).empty

Sprawdzenie pustej tabeli chroni przed założeniem, że zawsze znajdzie się wynik. Warto też zmienić wszystkie statusy na cancelled: raport powinien być pusty. Dodanie powtórzonego identyfikatora powinno zatrzymać funkcję. Takie przypadki są ciekawsze niż kolejne uruchomienie identycznych sześciu wierszy. Testują granice, na których często pojawiają się błędy raportowania.

Co zrobić, gdy pojawi się błąd

Przeczytaj nazwę wyjątku i ostatnie linie komunikatu. KeyError często kieruje uwagę na brakujący klucz lub kolumnę. ValueError w naszej funkcji wskazuje naruszone założenie. ModuleNotFoundError przy imporcie pandas może oznaczać uruchomienie innego interpretera niż ten, w którym zainstalowałeś bibliotekę. Zacznij od sprawdzenia konkretnego objawu, zamiast przepisywać cały program.

Zmniejsz przykład do kilku wierszy, na których problem nadal występuje. Wypisz nazwy kolumn i typy. Sprawdź, czy plik ma właściwy separator. Jeśli wszystkie dane wylądowały w jednej kolumnie, dalsze poprawianie grupowania nie naprawi importu. Ta kolejność pracy pomaga oddzielić błąd wejścia, błąd przekształcenia i błąd interpretacji.

W notatniku uruchom ponownie wszystkie komórki od początku. Stara zmienna może przetrwać zmianę wcześniejszego kodu i sprawić, że wynik będzie zależał od kolejności kliknięć. Zapisany skrypt, który działa w świeżej sesji, daje lepszy punkt odniesienia. Gdy prosisz o pomoc, pokaż małą syntetyczną próbkę, kod i oczekiwany wynik, bez danych klientów.

Przejście do własnego pliku

Zanim podmienisz syntetyczny zbiór na własny plik, zapisz krótki opis jego formatu. W jakim kodowaniu jest tekst? Jaki znak rozdziela kolumny? Czy kwota jest podana w groszach, czy w pełnych jednostkach? Czy eksport zawiera jeden dzień, czy całą historię? Dwie tabele o identycznych nazwach kolumn mogą reprezentować inne zakresy, więc same nagłówki nie wystarczą do kontroli wejścia.

Pracuj na kopii przeznaczonej do ćwiczenia i zachowaj oryginał. Nie poprawiaj ręcznie kilku wartości w pliku bez zapisania reguły, bo przy następnym eksporcie problem wróci. Jeśli przyczyna jest powtarzalna, opisz ją i dopiero wtedy dodaj kontrolowane przekształcenie. Jeżeli nie rozumiesz, skąd wzięła się wartość, oznacz przypadek do wyjaśnienia. Automatyzowanie nieznanej reguły sprawia głównie, że błąd powtarza się sprawniej.

Przy zapisywaniu raportu nadaj wynikowi osobną nazwę i dopisz zakres danych. Sam plik wynikowy powinien pozwalać ustalić, co policzono oraz czego suma nie obejmuje. W tym przykładzie zachowaj wszystkie trzy miary, zamiast eksportować wyłącznie ostatnią kolumnę. Bez liczby zamówień i liczby znanych kwot odbiorca nie rozpozna, że kanały mają różną kompletność. Możesz również dołączyć krótką notatkę o użytym filtrze statusu.

Jak czytać kod, którego jeszcze nie znasz

Weź pojedynczą linię i nazwij jej wejście oraz wyjście. Przed filtrem masz sześć wierszy, po filtrze pięć, a po grupowaniu trzy grupy. Każda zmiana liczby rekordów powinna mieć wyjaśnienie. Gdy rozbudujesz przykład, sprawdzaj te punkty pośrednie. To prostsze niż jednoczesne szukanie przyczyny błędu w imporcie, filtrze i agregacji.

Nie musisz pamiętać wszystkich argumentów biblioteki. Warto umieć znaleźć dokumentację właściwej operacji i odnieść przykład do własnego zbioru. Sprawdź zwracany typ, zachowanie braków oraz wartości domyślne. Jeśli wynik jest tabelą zamiast pojedynczej liczby, kolejny krok musi to uwzględniać. Czytanie dokumentacji staje się znacznie łatwiejsze, gdy masz przed sobą mały przypadek i jedno konkretne pytanie.

Na końcu wyjaśnij funkcję komuś bez znajomości Pythona: odrzucam dane naruszające założenia, wybieram opłacone zamówienia, grupuję je według kanału i pokazuję kompletność kwot. Jeśli potrafisz opisać ten przebieg zwykłymi słowami, łatwiej zauważysz, czy kod realizuje zamierzoną analizę. Składnia jest wtedy sposobem zapisania rozumowania, a każda instrukcja ma zrozumiały cel.

Następny krok: zmień pytanie, zachowaj kontrolę

Dodaj zamówienie 007 w kanale web, ze statusem paid i kwotą 5000. Najpierw zapisz przewidywanie: sześć opłaconych zamówień, cztery znane kwoty, suma znanych kwot 25000, w kanale web trzy zamówienia. Dopiero potem uruchom kod. Jeśli wynik jest inny, porównaj rekordy i znajdź przyczynę zamiast zmieniać oczekiwanie na to, co akurat pokazał komputer.

Następnie dodaj kolumnę daty i zaplanuj raport dla jednego miesiąca. Ustal, która data wyznacza okres i jak potraktujesz jej brak. Nie musisz od razu pisać bardziej skomplikowanej funkcji. Najpierw opisz regułę, przygotuj kilka granicznych rekordów i policz mały wynik ręcznie. W ten sposób uczysz się Pythona razem z projektowaniem analizy.

Po tym ćwiczeniu masz cały przebieg: wejście, kontrolę, wybór rekordów, grupowanie i sprawdzenie wyniku. Każdy kolejny element języka będzie łatwiej osadzić w zadaniu. Ja wróciłbym do tego samego kodu po kilku dniach i spróbował wyjaśnić go bez zaglądania do tekstu. Miejsca, których nie umiesz objaśnić, wyznaczą sensowny temat następnej sesji nauki.

Najważniejsze w pigułce

Czy muszę instalować Anacondę?

Nie. Do tego ćwiczenia wystarcza Python, osobne środowisko venv i pandas. W istniejącym zespole korzystaj z uzgodnionego środowiska.

Czym różnią się size i count w grupowaniu?

size liczy wiersze, a count w wybranej kolumnie liczy wartości niebrakujące. Dlatego liczba zamówień może być większa od liczby znanych kwot.

Czy brakującą kwotę mogę zamienić na zero?

Tylko jeśli wynika to z uzgodnionej definicji danych. W tym przykładzie brak oznacza nieznaną kwotę, więc zachowuję go i pokazuję kompletność.

Kajo Rudziński

Newsletter KajoData

Zostańmy w kontakcie.

Piszę o analizie danych, uczeniu się i zmianie pracy. Dzielę się tym, co pomaga mi ogarniać technologię i zawodowe decyzje. Jeśli lubisz ten sposób myślenia, zapraszam do mojego newslettera.