Pytania rekrutacyjne pandas

53 pytań — 20 odpowiedzi dostępnych od razu

Ten darmowy podgląd pytań rekrutacyjnych pandas pomaga powtórzyć zagadnienia związane z analizą i przetwarzaniem danych w Pythonie. Zawiera przykładowe pytania z zestawu 53 zagadnień.

Materiał dotyczy tworzenia i wczytywania danych, eksploracji, indeksowania, selekcji oraz czyszczenia braków. Fiszki pozwalają przećwiczyć pojęcia potrzebne w rozmowach o pracy z DataFrame.

Darmowy start

Pierwsze 20 odpowiedzi są dostępne od razu

Przeglądaj pełną listę 53 pytań. Pełne odpowiedzi, przykłady kodu i materiały do nauki odblokujesz w panelu.

Odblokuj 33 odpowiedzi

Podstawy pandas

Czym jest pandas i jakie problemy rozwiązuje w analizie i przetwarzaniu danych?

Odpowiedź w 30 sekund: pandas to biblioteka Pythona do pracy z danymi tabelarycznymi i szeregami czasowymi. Dostarcza dwie kluczowe struktury — Series (jednowymiarową) i DataFrame (dwuwymiarową) — oraz zestaw narzędzi do wczytywania, czyszczenia, transformacji, łączenia i agregacji danych. Rozwiązuje problem wygodnej, wydajnej i czytelnej manipulacji danymi, której czysty Python (listy, słowniki) nie zapewnia.

Odpowiedź w 2 minuty: pandas powstała, by dać analitykom i programistom w Pythonie narzędzie zbliżone wygodą do arkusza kalkulacyjnego, ale z mocą języka programowania i wydajnością obliczeń wektorowych. Zamiast pisać pętle po wierszach, operuje się na całych kolumnach naraz (operacje wektoryzowane oparte o NumPy), co jest jednocześnie szybsze i bardziej zwięzłe.

W praktyce pandas rozwiązuje cały cykl pracy z danymi: wczytywanie z wielu formatów (CSV, Excel, JSON, SQL, Parquet), czyszczenie (obsługa braków danych NaN, duplikatów, niespójnych typów), transformację (filtrowanie, dodawanie kolumn, zmiana kształtu), łączenie wielu zbiorów (merge, join, concat), grupowanie i agregację (groupby) oraz pracę z indeksem czasowym. Dzięki temu jest podstawą ekosystemu data science w Pythonie i naturalnym etapem przed wizualizacją (matplotlib, seaborn) czy uczeniem maszynowym (scikit-learn).

Typowe błędne przekonania: (1) że pandas jest „tylko ładnym Excelem" — w rzeczywistości jest w pełni programowalna, powtarzalna i skalowalna do milionów wierszy; (2) że pętla po wierszach (for/iterrows) to dobry sposób przetwarzania — niemal zawsze lepsze są operacje wektorowe; (3) że pandas jest dobra do danych większych niż RAM — przy bardzo dużych zbiorach lepiej sięgnąć po Dask, Polars lub bazę danych.

Przykład kodu:

import pandas as pd

# Wczytanie danych z pliku CSV
df = pd.read_csv("sprzedaz.csv")

# Szybki podgląd: pierwsze wiersze, typy kolumn, statystyki
print(df.head())        # pierwsze 5 wierszy
print(df.info())        # typy danych i liczba braków
print(df.describe())    # statystyki kolumn liczbowych

# Filtrowanie i agregacja w kilku liniach
sprzedaz_pln = df[df["waluta"] == "PLN"]
suma_wg_regionu = sprzedaz_pln.groupby("region")["kwota"].sum()
print(suma_wg_regionu)

Materiały

Czym różni się Series od DataFrame?

Odpowiedź w 30 sekund: Series to jednowymiarowa, etykietowana tablica wartości jednego typu — odpowiednik pojedynczej kolumny. DataFrame to dwuwymiarowa struktura tabelaryczna: zbiór kolumn (Series), które współdzielą jeden wspólny Index wierszy. Wyciągnięcie jednej kolumny z DataFrame zwraca Series.

Odpowiedź w 2 minuty: Series można sobie wyobrazić jako tablicę NumPy wzbogaconą o etykiety (indeks). Ma jeden wymiar, jeden typ danych (dtype) i własny Index, który pozwala adresować elementy po etykietach, a nie tylko po pozycji. Posiada też atrybut name, który po umieszczeniu w DataFrame staje się nazwą kolumny.

DataFrame to dwuwymiarowa tabela — kolekcja kolumn, z których każda jest osobną Series. Kluczowe jest to, że wszystkie kolumny dzielą ten sam Index wierszy. Każda kolumna może mieć inny typ danych (jedna int64, druga object/tekst, trzecia datetime64), podczas gdy w obrębie jednej kolumny typ jest spójny. DataFrame ma dwa wymiary etykiet: df.index (etykiety wierszy) oraz df.columns (etykiety kolumn).

Najczęstsze pułapki: (1) df["kolumna"] zwraca Series, a df[["kolumna"]] (lista z jedną nazwą) zwraca jednokolumnowy DataFrame — to różne typy; (2) wiele metod (np. apply, agregacje) zachowuje się inaczej na Series niż na DataFrame; (3) początkujący mylą jednowymiarowy charakter Series z jednokolumnowym DataFrame — to nie to samo.

Przykład kodu:

import pandas as pd

# Series — jeden wymiar, własny indeks, jeden typ
ceny = pd.Series([10.0, 25.5, 7.99], index=["a", "b", "c"], name="cena")
print(type(ceny))          # <class 'pandas.core.series.Series'>
print(ceny["b"])           # dostęp po etykiecie -> 25.5

# DataFrame — wiele kolumn (Series) o wspólnym indeksie
df = pd.DataFrame({
    "produkt": ["Jabłko", "Banan", "Wiśnia"],
    "cena": [10.0, 25.5, 7.99],
    "ilosc": [3, 1, 12],
})

# Wyciągnięcie kolumny -> Series
print(type(df["cena"]))    # <class 'pandas.core.series.Series'>

# Wyciągnięcie listy kolumn -> DataFrame (nawet jednokolumnowy)
print(type(df[["cena"]]))  # <class 'pandas.core.frame.DataFrame'>

Porównanie:

Cecha Series DataFrame
Wymiary 1 (jednowymiarowa) 2 (tabela)
Typy danych jeden dtype na całość różne dtype per kolumna
Etykiety jeden Index index (wiersze) + columns
Analogia jedna kolumna / wektor cała tabela / arkusz
Dostęp s["etykieta"] df["kolumna"], df.loc[...]

Diagram:

flowchart TB
    subgraph DF["DataFrame"]
        IDX["Index (wspólny dla wierszy)<br/>0, 1, 2"]
        C1["Series: 'produkt'<br/>dtype object"]
        C2["Series: 'cena'<br/>dtype float64"]
        C3["Series: 'ilosc'<br/>dtype int64"]
    end
    IDX --- C1
    IDX --- C2
    IDX --- C3

Materiały

Jaka jest relacja między pandas a NumPy i jak pandas wykorzystuje tablice NumPy „pod maską"?

Odpowiedź w 30 sekund: pandas jest zbudowana na NumPy — kolumny Series/DataFrame przechowują dane w tablicach ndarray (lub w nowszych typach ExtensionArray), a operacje wektorowe pandas delegują obliczenia do skompilowanego kodu NumPy. pandas dodaje na wierzchu etykietowane indeksy, obsługę braków danych, heterogeniczne typy kolumn i bogaty zestaw metod do analizy. Mówiąc skrótowo: NumPy to silnik liczbowy, a pandas to etykietowana, „tabelaryczna" warstwa nad nim.

Odpowiedź w 2 minuty: Historycznie pandas powstała jako warstwa wygody nad NumPy. Tablica NumPy (ndarray) jest szybka i pamięciowo zwarta, ale operuje na czystych pozycjach (0, 1, 2...) i zwykle jeden typ na całą tablicę. pandas dokłada do tego: etykietowane osie (Index), automatyczne wyrównywanie danych po etykietach, pierwszorzędną obsługę braków (NaN/NA) oraz możliwość trzymania w jednej tabeli kolumn różnych typów.

„Pod maską" klasyczny DataFrame przechowuje dane w blokach (BlockManager) — kolumny tego samego typu są często grupowane we wspólne tablice NumPy. Gdy wywołasz operację wektorową (np. df["a"] + df["b"] czy df["a"].sum()), pandas przekazuje ją do NumPy, które wykonuje obliczenia w skompilowanym C — dlatego operacje wektorowe są wielokrotnie szybsze niż pętle w Pythonie. Dostęp do surowych danych daje Series.to_numpy() lub df.to_numpy(); starszy atrybut .values działa podobnie, ale jest mniej zalecany dla nowych typów.

Warto znać niuanse: (1) od pandas 1.0+ istnieją typy nullable (Int64, boolean, string) oparte o ExtensionArray, które nie są zwykłym ndarray — pozwalają na braki danych bez konwersji do float; (2) backend Apache Arrow (dtype_backend="pyarrow") to alternatywa dla NumPy przy tekstach i dużych zbiorach; (3) mieszanie typów w DataFrame powoduje, że df.to_numpy() zwróci tablicę typu object, tracąc wydajność — to typowa pułapka. Mimo rosnącej roli Arrow, NumPy pozostaje fundamentem dla danych liczbowych w pandas.

Przykład kodu:

import pandas as pd
import numpy as np

s = pd.Series([1, 2, 3, 4], name="x")

# Pod spodem Series trzyma tablicę NumPy
print(type(s.to_numpy()))   # <class 'numpy.ndarray'>

# Funkcje NumPy działają bezpośrednio na obiektach pandas
print(np.sqrt(s))           # zwektoryzowane, liczone w C

# Operacja wektorowa delegowana do NumPy (bez pętli w Pythonie)
df = pd.DataFrame({"a": [1, 2, 3], "b": [10, 20, 30]})
df["suma"] = df["a"] + df["b"]   # szybkie dodawanie kolumn

# Surowe dane całego DataFrame jako tablica NumPy
arr = df.to_numpy()
print(arr.dtype)            # wspólny typ; przy mieszanych -> object

Diagram:

flowchart TB
    A["Twój kod pandas<br/>df['a'] + df['b']"] --> B["Warstwa pandas<br/>Index, etykiety, obsługa NaN, dtypes"]
    B --> C["BlockManager<br/>grupowanie kolumn w bloki"]
    C --> D["NumPy ndarray / ExtensionArray"]
    D --> E["Skompilowany kod C / SIMD<br/>obliczenia wektorowe"]

Materiały

Czym jest indeks (Index) w pandas i jaką pełni rolę przy dostępie do danych oraz wyrównywaniu (alignment)?

Odpowiedź w 30 sekund: Index to etykietowana oś Series i DataFrame — zbiór nazw przypisanych do wierszy (i, w postaci columns, do kolumn). Pełni dwie kluczowe role: pozwala adresować dane po etykietach (.loc) zamiast tylko po pozycji oraz umożliwia automatyczne wyrównywanie (alignment) danych po wspólnych etykietach podczas operacji arytmetycznych i łączenia.

Odpowiedź w 2 minuty: Index to niemutowalny obiekt podobny do tablicy, przechowujący etykiety osi. W DataFrame istnieją dwa indeksy: df.index (etykiety wierszy) i df.columns (etykiety kolumn). Indeksem mogą być liczby, napisy, daty (DatetimeIndex), a nawet wiele poziomów naraz (MultiIndex). Domyślnie pandas tworzy RangeIndex (0, 1, 2...), ale często ustawia się własny indeks znaczący, np. identyfikator klienta czy znacznik czasu, przez set_index.

Najważniejsza, często niedoceniana, funkcja indeksu to wyrównywanie danych (alignment). Gdy wykonujesz operację na dwóch obiektach pandas — np. dodawanie dwóch Series — pandas najpierw dopasowuje je po etykietach indeksu, a nie po pozycji. Dzięki temu sumowane są wartości o tej samej etykiecie, niezależnie od ich kolejności, a tam, gdzie etykieta występuje tylko w jednym obiekcie, wynikiem jest NaN. To samo dotyczy przypisywania kolumn czy łączenia operacji join. Alignment eliminuje całą klasę cichych błędów znanych z arkuszy, gdzie wiersze trzeba ręcznie układać w tej samej kolejności.

Indeks ma też wpływ na wydajność i poprawność: dobrze dobrany, unikalny indeks przyspiesza wyszukiwanie (.loc) i scalanie; indeks z duplikatami może dawać niejednoznaczne wyniki. Typowe pułapki: (1) mylenie dostępu po etykiecie (.loc) z dostępem po pozycji (.iloc) — przy indeksie liczbowym łatwo o pomyłkę; (2) zapomnienie o reset_index() po groupby/set_index, gdy indeks staje się niewygodny; (3) zaskoczenie wartościami NaN po operacji na obiektach o różnych indeksach — to nie błąd, lecz właśnie efekt wyrównywania.

Przykład kodu:

import pandas as pd

# Ustawienie znaczącego indeksu zamiast domyślnego 0,1,2...
df = pd.DataFrame({
    "klient": ["Anna", "Bartek", "Celina"],
    "obroty": [1200, 800, 1500],
})
df = df.set_index("klient")

# Dostęp po etykiecie (.loc) vs po pozycji (.iloc)
print(df.loc["Bartek"])     # po etykiecie
print(df.iloc[0])           # po pozycji (pierwszy wiersz)

# Wyrównywanie (alignment) po etykietach indeksu
q1 = pd.Series({"Anna": 100, "Bartek": 200})
q2 = pd.Series({"Bartek": 50, "Celina": 70})

razem = q1 + q2
print(razem)
# Anna      NaN   <- brak w q2
# Bartek  250.0   <- 200 + 50, dopasowane po etykiecie
# Celina    NaN   <- brak w q1

# Aby uniknąć NaN, podaj wartość wypełnienia
razem2 = q1.add(q2, fill_value=0)
print(razem2)   # Anna 100, Bartek 250, Celina 70

Materiały

Czym pandas różni się od arkusza kalkulacyjnego i relacyjnej bazy danych z perspektywy analityka?

Odpowiedź w 30 sekund: pandas łączy interaktywność arkusza z programowalnością i powtarzalnością kodu. W odróżnieniu od arkusza (np. Excela) każda transformacja jest zapisana jako kod — powtarzalny, wersjonowalny i skalowalny do milionów wierszy. W odróżnieniu od relacyjnej bazy (SQL) pandas działa w pamięci procesu, ma uporządkowany indeks oraz pełnię Pythona do dowolnej logiki, ale gorzej radzi sobie z danymi większymi niż RAM i z trwałym, współbieżnym przechowywaniem.

Odpowiedź w 2 minuty: Wobec arkusza kalkulacyjnego główna różnica to powtarzalność i skala. W Excelu manipulacje wykonuje się ręcznie, mieszając dane z formułami w tych samych komórkach, co utrudnia audyt i odtworzenie wyniku. W pandas proces opisuje kod: ten sam skrypt da identyczny wynik na nowych danych, można go testować, wersjonować w git i uruchamiać automatycznie. pandas obsługuje też znacznie większe zbiory niż praktyczny limit arkusza i daje pełną moc Pythona (pętle warunkowe, funkcje, integracje, uczenie maszynowe). Arkusz wygrywa za to natychmiastową wizualną interaktywnością i niskim progiem wejścia dla osób nietechnicznych.

Wobec relacyjnej bazy danych pandas też ma wiele wspólnego — merge odpowiada JOIN, groupby to GROUP BY, filtrowanie to WHERE. Różnice są jednak istotne. Baza danych jest zaprojektowana do trwałego, spójnego, współbieżnego przechowywania danych (transakcje ACID, integralność, dostęp wielu użytkowników) i potrafi pracować na zbiorach znacznie większych niż pamięć dzięki optymalizatorowi zapytań i indeksom dyskowym. pandas natomiast trzyma wszystko w RAM jednego procesu — jest szybka i elastyczna do eksploracji oraz transformacji, ale nie jest magazynem danych. Dodatkowo pandas zachowuje kolejność wierszy i ma uporządkowany Index z natywnym wyrównywaniem, podczas gdy w SQL relacja jest z definicji zbiorem nieuporządkowanym.

W praktyce te narzędzia się uzupełniają: typowy przepływ to wyciągnięcie danych z bazy (SQL) lub arkusza, wczytanie do pandas (pd.read_sql, pd.read_excel), przetworzenie i analiza w Pythonie, a następnie zapis wyniku z powrotem lub do raportu. Pułapki analityka: (1) traktowanie pandas jak bazy danych przy danych większych niż RAM (lepiej Dask/Polars/SQL); (2) przenoszenie do Pythona całej tabeli, którą baza przefiltrowałaby taniej po stronie serwera; (3) zakładanie, że wyniki SQL przyjdą w określonej kolejności bez ORDER BY.

Porównanie:

Cecha Arkusz (Excel) pandas Baza relacyjna (SQL)
Powtarzalność niska (ręczne kliknięcia) wysoka (kod, git) wysoka (zapytania)
Skala danych tysiące–setki tys. wierszy do rozmiaru RAM bardzo duża (dysk)
Trwałość / współbieżność plik brak (w pamięci) mocna (ACID, wielu użytkowników)
Kolejność / indeks wizualna uporządkowany Index zbiór nieuporządkowany
Elastyczność logiki formuły, makra pełnia Pythona SQL + procedury
Próg wejścia bardzo niski średni średni–wysoki

Przykład kodu:

import pandas as pd

# Te same operacje w stylu SQL, ale w pandas

# WHERE region = 'PL'
filtr = df[df["region"] == "PL"]

# GROUP BY region; SUM(kwota)
agg = df.groupby("region")["kwota"].sum()

# JOIN po kluczu 'id_klienta'
polaczone = pd.merge(zamowienia, klienci, on="id_klienta", how="left")

# Most między światami: wczytaj z bazy, przetwórz, zapisz do Excela
# df = pd.read_sql("SELECT * FROM sprzedaz WHERE rok = 2025", conn)
# df.to_excel("raport.xlsx", index=False)

Materiały

Tworzenie i wczytywanie danych

Jakie znasz sposoby utworzenia obiektu DataFrame (ze słownika, listy słowników, tablicy NumPy, listy krotek)?

Odpowiedź w 30 sekund: DataFrame najczęściej tworzy się z słownika kolumn ({nazwa: lista_wartości}), gdzie klucze stają się nazwami kolumn. Można go też zbudować z listy słowników (każdy słownik to jeden wiersz), z tablicy NumPy 2D (z osobnym argumentem columns) oraz z listy krotek. Wybór zależy od tego, czy dane mamy „kolumnowo" (słownik), czy „wierszowo" (lista słowników/krotek).

Odpowiedź w 2 minuty: Konstruktor pd.DataFrame(...) jest bardzo elastyczny i akceptuje wiele struktur wejściowych. Najbardziej naturalny dla pandas jest słownik kolumn — klucze to nazwy kolumn, a wartości to listy/tablice o równej długości. To podejście jest czytelne i wydajne, bo pandas wewnętrznie przechowuje dane kolumnowo. Gdy dane przychodzą „wierszowo" (np. z API zwracającego rekordy JSON), wygodniejsza jest lista słowników — każdy słownik to jeden wiersz, a pandas automatycznie scala wszystkie klucze w komplet kolumn, wstawiając NaN tam, gdzie dany klucz w rekordzie nie wystąpił.

Z tablicy NumPy (2D) tworzymy DataFrame, podając dane oraz osobno columns i ewentualnie index. To popularne w obliczeniach naukowych, ale uwaga: tablica NumPy ma jeden wspólny dtype, więc liczby i tekst zmieszane w jednej tablicy staną się typem object. Lista krotek działa jak lista wierszy bez nazw — trzeba samodzielnie podać columns, inaczej pandas użyje domyślnych etykiet liczbowych.

Najczęstsze pułapki: (1) niezgodne długości list w słowniku kolumn rzucają błąd; (2) lista słowników z brakującymi kluczami daje NaN — to bywa pożądane, ale czasem maskuje błąd w danych; (3) kolejność kolumn z listy słowników w nowszych wersjach pandas jest zachowana zgodnie z pierwszym wystąpieniem klucza; (4) przy tablicy NumPy łatwo zgubić informację o typach. Dla bardzo wielu wierszy budowanych pojedynczo unikaj wielokrotnego append/konkatenacji w pętli — zbierz dane do listy i zbuduj DataFrame raz.

Przykład kodu:

import pandas as pd
import numpy as np

# 1) Ze słownika kolumn — klucze to nazwy kolumn (najbardziej naturalne)
df_slownik = pd.DataFrame({
    "imie": ["Anna", "Bartek", "Celina"],
    "wiek": [30, 25, 41],
    "miasto": ["Kraków", "Gdańsk", "Wrocław"],
})

# 2) Z listy słowników — każdy słownik to jeden wiersz
#    Brakujące klucze stają się NaN
rekordy = [
    {"imie": "Anna", "wiek": 30},
    {"imie": "Bartek", "wiek": 25, "miasto": "Gdańsk"},
]
df_rekordy = pd.DataFrame(rekordy)

# 3) Z tablicy NumPy 2D — trzeba podać nazwy kolumn osobno
tablica = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
df_numpy = pd.DataFrame(tablica, columns=["x", "y"])

# 4) Z listy krotek — wiersze bez nazw, kolumny podajemy ręcznie
krotki = [("Anna", 30), ("Bartek", 25), ("Celina", 41)]
df_krotki = pd.DataFrame(krotki, columns=["imie", "wiek"])

print(df_slownik)
print(df_rekordy)

Materiały

Jak wczytać dane z pliku CSV i które parametry read_csv są najważniejsze (sep, header, dtype, parse_dates, usecols)?

Odpowiedź w 30 sekund: Podstawowo pd.read_csv("plik.csv") wystarczy, ale realne pliki wymagają dostrojenia: sep ustala separator (np. ; w polskich plikach z Excela), header mówi, w którym wierszu są nazwy kolumn, dtype wymusza typy, parse_dates parsuje kolumny dat, a usecols wczytuje tylko wybrane kolumny. Dobranie tych parametrów decyduje o poprawności typów i zużyciu pamięci.

Odpowiedź w 2 minuty: read_csv to najczęściej używana funkcja IO w pandas i ma kilkadziesiąt parametrów, ale w praktyce kluczowych jest pięć. sep (alias delimiter) definiuje znak rozdzielający — domyślnie przecinek, ale polskie pliki eksportowane z Excela często używają średnika (sep=";"), a do plików z tabulatorami stosujemy sep="\t". header wskazuje wiersz z nazwami kolumn (header=0 to domyślnie pierwszy wiersz); header=None oznacza brak nagłówka i wtedy warto podać names=[...]. Częsta sytuacja to pliki z liniami opisu na górze — wtedy używamy skiprows.

dtype pozwala wymusić typy kolumn już przy wczytywaniu — to ważne dla wydajności (np. dtype={"kod": "category", "id": "int32"}) oraz dla poprawności (kody pocztowe czy NIP-y z wiodącymi zerami trzeba czytać jako str, inaczej pandas zrobi z nich liczby i obetnie zera). parse_dates zamienia wskazane kolumny na typ datetime64, dzięki czemu można na nich robić operacje czasowe; warto połączyć z date_format (nowsze pandas) dla jednoznacznego formatu i szybkości. usecols ogranicza wczytywanie do podanych kolumn (po nazwie lub indeksie) — to najprostszy sposób na oszczędność pamięci i czasu przy szerokich plikach.

Pułapki, o których warto pamiętać: kodowanie znaków (polskie pliki bywają w cp1250 lub latin2, nie zawsze w utf-8 — ustaw encoding), reprezentacja braków (na_values, np. ["", "brak", "NULL"]), separator dziesiętny (decimal="," w danych z polskim formatem liczb) oraz separator tysięcy (thousands=" "). Domyślne wnioskowanie typów potrafi „zepsuć" identyfikatory tekstowe i daty, dlatego na produkcji niemal zawsze warto jawnie deklarować dtype i parse_dates. Przy bardzo dużych plikach dochodzi jeszcze chunksize (osobne pytanie) oraz wybór szybszego silnika (engine="pyarrow" w nowszych wersjach).

Przykład kodu:

import pandas as pd

df = pd.read_csv(
    "sprzedaz.csv",
    sep=";",                       # polski plik z Excela używa średnika
    header=0,                      # nazwy kolumn w pierwszym wierszu
    usecols=["data", "kod", "kwota", "ilosc"],  # tylko potrzebne kolumny
    dtype={
        "kod": "string",          # zachowaj wiodące zera w kodzie
        "ilosc": "int32",         # mniejszy typ = mniej pamięci
    },
    parse_dates=["data"],          # zamień na datetime64
    decimal=",",                   # polski separator dziesiętny
    thousands=" ",                 # spacja jako separator tysięcy
    na_values=["", "brak", "NULL"],  # wartości traktowane jako NaN
    encoding="utf-8",              # ustaw świadomie (czasem cp1250)
)

print(df.dtypes)
print(df.head())

Materiały

Jak pandas obsługuje inne formaty wejściowe (Excel, JSON, SQL, Parquet) i kiedy wybrać Parquet zamiast CSV?

Odpowiedź w 30 sekund: pandas ma rodzinę funkcji read_*: read_excel (arkusze .xlsx/.xls, wybór sheet_name), read_json (różne orientacje, też linie JSON), read_sql (zapytania do baz przez SQLAlchemy) i read_parquet (kolumnowy format binarny). Parquet wybieramy zamiast CSV, gdy zależy nam na rozmiarze, szybkości i zachowaniu typów — CSV zostawiamy do wymiany danych z ludźmi i narzędziami, które rozumieją tylko tekst.

Odpowiedź w 2 minuty: Excel czytamy przez pd.read_excel(...), podając sheet_name (nazwę, indeks, listę albo None po wszystkie arkusze jako słownik). Pod spodem działa silnik — openpyxl dla .xlsx, dla starych .xls potrzebny jest inny pakiet. Excel bywa kapryśny: scalone komórki, formatowanie i automatyczne typy potrafią zaskoczyć, dlatego również tu warto kontrolować dtype i parse_dates. JSON obsługuje pd.read_json(...) z parametrem orient (np. records, split, columns) — dla zagnieżdżonych struktur lepiej sprawdza się pd.json_normalize(...), które „spłaszcza" obiekty do kolumn. Dla strumieni linia-po-linii używamy lines=True (format JSON Lines).

SQL odczytujemy funkcjami read_sql, read_sql_query i read_sql_table, przekazując połączenie (zwykle silnik SQLAlchemy) oraz zapytanie lub nazwę tabeli. To pozwala filtrować i agregować po stronie bazy (wydajniej niż wczytywać wszystko), a chunksize umożliwia strumieniowe pobieranie dużych wyników. Parquet czytamy read_parquet (silnik pyarrow lub fastparquet) — to kolumnowy, skompresowany format binarny, który przechowuje schemat i typy razem z danymi.

Kiedy Parquet zamiast CSV? Gdy dane są duże, czytane wielokrotnie i pozostają „wewnątrz" pipeline'u analitycznego. Parquet jest zwykle kilka razy mniejszy (kompresja kolumnowa), wczytuje się szybciej, zachowuje dokładne typy (daty, kategorie, liczby zmiennoprzecinkowe bez utraty precyzji) i pozwala czytać tylko wybrane kolumny bez parsowania całości. CSV ma jednak realne zalety: jest tekstowy, czytelny dla człowieka, otwiera się w dowolnym edytorze i Excelu, i jest uniwersalny przy wymianie danych. Pułapki: CSV gubi typy (wszystko jest tekstem do sparsowania) i jest podatny na problemy z kodowaniem oraz separatorami; Parquet wymaga dodatkowej biblioteki (pyarrow) i nie da się go sensownie obejrzeć bez kodu. Praktyczna reguła: do archiwizacji i przetwarzania — Parquet, do raportów i wymiany z ludźmi — CSV/Excel.

Przykład kodu:

import pandas as pd
from sqlalchemy import create_engine

# Excel — wybrany arkusz, parsowanie dat
df_xlsx = pd.read_excel("raport.xlsx", sheet_name="Sprzedaz", parse_dates=["data"])

# JSON — format records oraz spłaszczanie struktur zagnieżdżonych
df_json = pd.read_json("dane.json", orient="records")
df_plaski = pd.json_normalize(
    [{"id": 1, "klient": {"miasto": "Kraków", "nip": "123"}}],
    sep="_",
)  # kolumny: id, klient_miasto, klient_nip

# SQL — zapytanie przez SQLAlchemy (filtrowanie po stronie bazy)
silnik = create_engine("postgresql+psycopg2://user:haslo@host:5432/baza")
df_sql = pd.read_sql_query(
    "SELECT data, kwota FROM sprzedaz WHERE rok = 2025",
    con=silnik,
    parse_dates=["data"],
)

# Parquet — szybki, kolumnowy, zachowuje typy; czytaj tylko wybrane kolumny
df_parquet = pd.read_parquet("dane.parquet", columns=["data", "kwota"])

Tabela porównawcza CSV vs Parquet:

Cecha CSV Parquet
Rozmiar na dysku duży (tekst, brak kompresji) mały (kompresja kolumnowa, często 3-10x mniejszy)
Zachowanie typów brak — wszystko to tekst do parsowania pełne — schemat i typy zapisane razem z danymi
Szybkość odczytu wolniejszy (parsowanie tekstu) szybszy (binarny, odczyt wybranych kolumn)
Odczyt wybranych kolumn wczytuje cały wiersz, potem filtruje natywny — czyta tylko potrzebne kolumny
Czytelność dla człowieka wysoka (zwykły edytor, Excel) brak — wymaga kodu/biblioteki
Uniwersalność / wymiana bardzo wysoka (każde narzędzie) wymaga pyarrow/fastparquet
Najlepsze zastosowanie wymiana z ludźmi, raporty, mały eksport archiwizacja, duże dane, pipeline analityczny

Materiały

Jak zapisać DataFrame do pliku i na co zwrócić uwagę przy eksporcie (kodowanie, indeks, separator)?

Odpowiedź w 30 sekund: Każdej funkcji read_* odpowiada metoda to_*: df.to_csv(...), df.to_excel(...), df.to_parquet(...), df.to_json(...). Przy CSV najczęstsze decyzje to: index=False (żeby nie zapisywać kolumny indeksu), encoding="utf-8" (lub utf-8-sig dla Excela) oraz sep (separator). Dobrze dobrane parametry sprawiają, że plik bez problemu otworzy się w docelowym narzędziu.

Odpowiedź w 2 minuty: Eksport jest lustrzanym odbiciem importu, ale ma kilka pułapek, które łatwo przeoczyć. Najważniejsza przy to_csv to indeks: domyślnie pandas dopisuje kolumnę z indeksem, co po ponownym wczytaniu daje zbędną kolumnę „Unnamed: 0". Jeśli indeks nie niesie informacji (jest zwykłym licznikiem), używaj index=False. Kodowanie ma znaczenie dla polskich znaków: utf-8 to standard, ale gdy plik ma być otwierany dwukrotnym kliknięciem w Excelu na Windows, lepiej utf-8-sig (dodaje BOM, dzięki czemu Excel poprawnie rozpozna ogonki). Separator (sep) dobieramy do odbiorcy — średnik (;) bywa wymagany dla polskiego Excela, który przecinek interpretuje jako separator dziesiętny.

Dalsze kwestie to: format separatora dziesiętnego (decimal="," dla polskich raportów), sposób zapisu braków (na_rep="brak"), wybór kolumn (columns=[...]), nagłówek (header=False, gdy nie chcemy nazw) i format dat (date_format="%Y-%m-%d"). Przy to_excel warto zwrócić uwagę na sheet_name, na to, że również tam domyślnie zapisywany jest indeks (index=False), oraz na to, że wiele arkuszy zapisuje się przez pd.ExcelWriter. Dla Parquet zwykle wystarczy df.to_parquet("plik.parquet") — format sam przechowuje typy i schemat, więc nie ma problemu z kodowaniem ani separatorami; można dodatkowo wybrać kompresję (compression="snappy" lub "zstd").

Najczęstsze błędy w praktyce: zapomniany index=False (śmieciowa kolumna po round-tripie), zły encoding (krzaki zamiast polskich znaków), zapis przecinkiem dla pliku, który użytkownik otworzy w polskim Excelu, oraz utrata typów przy CSV (data zapisana jako tekst). Jeśli plik ma wrócić do pandas bez strat typów, zamiast CSV wybierz Parquet. Pamiętaj też o ścieżce zapisu i kompresji — to_csv("plik.csv.gz") zapisze od razu skompresowany plik.

Przykład kodu:

import pandas as pd

df = pd.DataFrame({
    "data": pd.to_datetime(["2025-01-10", "2025-02-15"]),
    "kwota": [1234.50, 9876.00],
})

# CSV dla polskiego Excela: średnik, przecinek dziesiętny, BOM, bez indeksu
df.to_csv(
    "raport.csv",
    index=False,            # nie zapisuj kolumny indeksu
    sep=";",                # separator akceptowany przez polski Excel
    decimal=",",            # polski separator dziesiętny
    encoding="utf-8-sig",   # BOM => Excel poprawnie pokaże ą, ę, ł
    na_rep="brak",          # czytelna reprezentacja braków
    date_format="%Y-%m-%d", # jednoznaczny format daty
)

# Excel — także bez indeksu, z nazwą arkusza
df.to_excel("raport.xlsx", index=False, sheet_name="Sprzedaz")

# Parquet — bezstratny round-trip typów, dobra kompresja
df.to_parquet("raport.parquet", compression="zstd")

Materiały

Jak radzić sobie z plikiem zbyt dużym, by wczytać go w całości do pamięci (parametr chunksize, iteracyjne przetwarzanie)?

Odpowiedź w 30 sekund: Najprostsze rozwiązanie to wczytywanie pliku w kawałkach (pd.read_csv(..., chunksize=N)), które zwraca iterator porcji DataFrame — przetwarzasz każdą porcję osobno i agregujesz wynik, nigdy nie trzymając całości w RAM. Dodatkowo warto wczytywać tylko potrzebne kolumny (usecols), zawężać typy (dtype, category) oraz rozważyć Parquet lub narzędzia out-of-core (Dask, Polars), gdy danych jest bardzo dużo.

Odpowiedź w 2 minuty: Gdy plik nie mieści się w pamięci, kluczowa jest zasada: nie wczytuj wszystkiego naraz, tylko strumieniuj i agreguj. Parametr chunksize w read_csv (i read_sql, read_json z lines=True) sprawia, że funkcja zwraca obiekt iteracyjny TextFileReader — w pętli for chunk in reader: dostajesz kolejne kawałki po N wierszy. Każdy kawałek przetwarzasz (filtruj, licz sumy, grupuj) i odkładasz tylko mały wynik częściowy, a po pętli łączysz wyniki. Dzięki temu szczytowe zużycie pamięci jest proporcjonalne do rozmiaru jednego kawałka, a nie całego pliku.

Przed sięgnięciem po chunksize warto najpierw zmniejszyć zapotrzebowanie na pamięć: usecols ogranicza kolumny, dtype wymusza węższe typy (int32 zamiast int64, float32 zamiast float64), a kolumny o niskiej liczności warto czytać jako category. Często samo to wystarcza, by plik się zmieścił. Jeśli nie — łączymy oba podejścia: wąskie typy plus przetwarzanie w kawałkach. Przy agregacjach trzeba uważać, by wybierać operacje rozkładalne (sumy, liczności, min/max, sumy częściowe do średniej ważonej) — wartości jak mediana czy dokładna liczba unikalnych elementów wymagają specjalnego podejścia (np. zbieranie zbioru unikatów lub algorytmy przybliżone).

Alternatywy i pułapki: dla powtarzalnych analiz lepiej raz przekonwertować dane do Parquet (mniejszy, szybszy, kolumnowy odczyt) i czytać tylko potrzebne kolumny. Gdy dane przerastają możliwości jednej maszyny lub potrzebujemy równoległości, sięgamy po narzędzia out-of-core/leniwe: Dask (API podobne do pandas, leniwe i równoległe), Polars (bardzo szybki, tryb scan_* leniwy) albo przetwarzanie po stronie bazy SQL (filtrowanie i agregacja w WHERE/GROUP BY, zanim dane trafią do Pythona). Typowe błędy to: zbyt mały chunksize (narzut na wielu iteracjach), zbyt duży (znów brak pamięci), gromadzenie wszystkich kawałków w liście i łączenie ich na końcu (to neguje zysk z chunkowania) oraz próba liczenia metryk niekomutatywnych bez właściwego algorytmu. Dobry punkt startowy to chunksize rzędu 10^5–10^6 wierszy i dostrojenie pod realny rozmiar wiersza.

Przykład kodu:

import pandas as pd

# Strumieniowe sumowanie kwoty wg kategorii bez wczytywania całości do RAM
sumy_czesciowe = []  # tylko małe wyniki częściowe, nie całe kawałki

czytnik = pd.read_csv(
    "ogromny_plik.csv",
    chunksize=500_000,                 # liczba wierszy na jeden kawałek
    usecols=["kategoria", "kwota"],    # tylko potrzebne kolumny
    dtype={"kategoria": "category", "kwota": "float32"},  # wąskie typy
)

for kawalek in czytnik:
    # przetwarzaj pojedynczy kawałek (mały fragment w pamięci)
    czesc = kawalek.groupby("kategoria", observed=True)["kwota"].sum()
    sumy_czesciowe.append(czesc)

# Połącz wyniki częściowe i zsumuj po kategoriach (operacja rozkładalna)
wynik = pd.concat(sumy_czesciowe).groupby(level=0).sum()
print(wynik)

Diagram:

flowchart TD
    A["Otwórz plik z chunksize=N"] --> B["Pobierz kolejny kawałek (N wierszy)"]
    B --> C["Przetwórz kawałek (filtruj, agreguj)"]
    C --> D["Dołącz wynik częściowy do akumulatora"]
    D --> E{"Czy są kolejne wiersze?"}
    E -- "Tak" --> B
    E -- "Nie" --> F["Połącz wyniki częściowe (np. suma sum)"]
    F --> G["Zwróć wynik końcowy"]

Materiały

Inspekcja i eksploracja danych

Jakich metod używasz do wstępnego przeglądu danych (head, tail, info, describe, shape)?

Odpowiedź w 30 sekund: Po wczytaniu danych zaczynam od df.shape (ile wierszy i kolumn), df.head()/df.tail() (podgląd pierwszych i ostatnich rekordów), df.info() (typy kolumn i liczba wartości niepustych) oraz df.describe() (statystyki opisowe). Ta sekwencja w kilkanaście sekund daje obraz rozmiaru, struktury, typów i jakości zbioru.

Odpowiedź w 2 minuty: Wstępny przegląd danych (tzw. pierwszy rzut oka, część EDA) ma odpowiedzieć na pytania: jak duży jest zbiór, jakie ma kolumny, jakiego typu są dane i czy nie ma w nich oczywistych problemów. Każda z metod pełni tu inną rolę i warto je traktować jako uzupełniający się zestaw, a nie wymienne.

df.shape zwraca krotkę (liczba_wierszy, liczba_kolumn) — to pierwsza informacja, która mówi mi o skali danych i o tym, czy import w ogóle się powiódł (np. czy nie wczytałem wszystkiego do jednej kolumny przez zły separator). df.head(n) i df.tail(n) pokazują domyślnie 5 pierwszych/ostatnich wierszy — pozwalają zobaczyć realne wartości, format dat, jednostki, ewentualne wiersze nagłówkowe lub podsumowujące doklejone na końcu pliku. df.sample(n) jest cennym uzupełnieniem, bo losowa próbka czasem ujawnia problemy ukryte w środku zbioru, których nie widać na początku ani końcu.

df.info() to moja najważniejsza metoda diagnostyczna: pokazuje nazwy kolumn, ich dtype, liczbę wartości niepustych (z czego od razu wnioskuję o brakach) oraz zużycie pamięci. Jeśli kolumna, która powinna być liczbą, ma typ object, to sygnał, że gdzieś są wartości tekstowe lub błędne separatory dziesiętne. df.describe() domyślnie liczy statystyki tylko dla kolumn numerycznych (liczność, średnia, odchylenie, min, kwartyle, max). Aby objąć też kolumny tekstowe i kategoryczne, używam df.describe(include='all'), co dodaje unique, top i freq. Dobrym nawykiem jest też df.columns, df.dtypes oraz df.index, gdy interesuje mnie struktura, a nie wartości.

Przykład kodu:

import pandas as pd

df = pd.read_csv("sprzedaz.csv")

# Skala zbioru: (liczba_wierszy, liczba_kolumn)
print(df.shape)

# Podgląd realnych wartości z początku i końca
df.head()        # domyślnie 5 wierszy
df.tail(3)       # ostatnie 3 wiersze
df.sample(5)     # losowa próbka — ujawnia problemy w środku zbioru

# Typy, liczba wartości niepustych, zużycie pamięci
df.info()

# Statystyki opisowe — tylko kolumny numeryczne
df.describe()

# Statystyki dla WSZYSTKICH kolumn (też tekstowych/kategorycznych)
df.describe(include="all")

# Szybki przegląd struktury bez wartości
df.dtypes        # typy kolumn
df.columns       # nazwy kolumn

Materiały

Jak sprawdzić i zmienić typy danych kolumn oraz dlaczego właściwe dtype ma znaczenie?

Odpowiedź w 30 sekund: Typy sprawdzam przez df.dtypes lub df.info(), a zmieniam głównie metodą df.astype(), a dla przypadków „nieczystych" danych funkcjami pd.to_numeric, pd.to_datetime i pd.to_timedelta z parametrem errors='coerce'. Właściwy dtype ma znaczenie, bo decyduje o poprawności obliczeń, możliwości sortowania i filtrowania, zużyciu pamięci i wydajności operacji.

Odpowiedź w 2 minuty: Sprawdzenie typów to punkt wyjścia. df.dtypes zwraca typ każdej kolumny, a df.info() dodatkowo pokazuje liczbę wartości niepustych. Typ object to najczęściej napisy (albo mieszanka różnych typów) i bardzo często jest sygnałem, że dane wymagają konwersji — np. liczby zapisane jako tekst z przecinkiem dziesiętnym albo daty w formacie tekstowym.

Do konwersji służą dwa podejścia. df['kol'].astype(typ) jest proste i szybkie, ale wymaga, żeby dane dało się „twardo" przekonwertować — przy błędnej wartości rzuci wyjątek. Dlatego dla danych z realnego świata bezpieczniejsze są dedykowane funkcje: pd.to_numeric, pd.to_datetime, pd.to_timedelta. Mają one parametr errors='coerce', który zamienia niekonwertowalne wartości na NaN/NaT zamiast przerywać przetwarzanie — dzięki temu od razu widzę, ile rekordów było problematycznych. Dla danych o dużej liczbie powtórzeń (np. kategorie, płeć, status) warto użyć typu category, który drastycznie zmniejsza zużycie pamięci i przyspiesza grupowanie. Do reprezentacji brakujących liczb całkowitych służą typy nullable (Int64, Float64, boolean) — pozwalają trzymać NA w kolumnie całkowitoliczbowej, czego klasyczny int64 nie potrafi.

Dlaczego to ważne? Po pierwsze, poprawność: jeśli kwoty są typu object, to sum() skonkatenuje napisy zamiast je dodać, a sortowanie zadziała leksykograficznie („100" < „99"). Po drugie, funkcjonalność: dopiero kolumna typu datetime64 udostępnia akcesor .dt (np. .dt.year, .dt.dayofweek) i pozwala na poprawne filtrowanie zakresów dat. Po trzecie, wydajność i pamięć: zmiana float64 na float32 czy object na category potrafi zmniejszyć zużycie pamięci wielokrotnie, co ma realne znaczenie przy dużych zbiorach. Dobrą praktyką jest też ustawienie właściwych typów już przy wczytywaniu (dtype=, parse_dates= w read_csv), zamiast naprawiać je później.

Przykład kodu:

import pandas as pd

# Sprawdzenie typów
df.dtypes
df.info()

# 1) Prosta, "twarda" konwersja — rzuci wyjątek przy błędnej wartości
df["wiek"] = df["wiek"].astype("int64")

# 2) Bezpieczna konwersja danych "nieczystych":
#    niekonwertowalne wartości -> NaN/NaT zamiast błędu
df["cena"] = pd.to_numeric(df["cena"], errors="coerce")
df["data"] = pd.to_datetime(df["data"], errors="coerce", format="%Y-%m-%d")

# 3) Liczby z przecinkiem dziesiętnym zapisane jako tekst
df["kwota"] = (df["kwota"].str.replace(" ", "", regex=False)
                          .str.replace(",", ".", regex=False)
                          .pipe(pd.to_numeric, errors="coerce"))

# 4) Typ kategoryczny — oszczędność pamięci dla powtarzalnych wartości
df["status"] = df["status"].astype("category")

# 5) Typ nullable — liczby całkowite z możliwością przechowania NA
df["liczba"] = df["liczba"].astype("Int64")

# Najlepiej: ustawić typy już przy wczytywaniu
df = pd.read_csv(
    "dane.csv",
    dtype={"status": "category", "kod": "string"},
    parse_dates=["data"],
)

Materiały

Jak działa value_counts i w jakich sytuacjach jest przydatne?

Odpowiedź w 30 sekund: value_counts() zlicza wystąpienia każdej unikalnej wartości w serii i zwraca wynik posortowany malejąco. Najczęściej używam go do poznania rozkładu kolumny kategorycznej, znalezienia literówek i niespójności w danych oraz — z normalize=True — do obliczenia udziałów procentowych. Pamiętam o dropna=False, żeby zobaczyć też liczbę braków.

Odpowiedź w 2 minuty: value_counts() działa na serii (kolumnie) i zwraca nową serię, w której indeksem są unikalne wartości, a wartościami liczba ich wystąpień, posortowana domyślnie od najczęstszej. To jeden z najszybszych sposobów na zrozumienie, „co właściwie jest w tej kolumnie". Domyślnie pomija wartości NaN, dlatego przy ocenie jakości danych prawie zawsze ustawiam dropna=False, żeby braki też były policzone i widoczne jako osobna pozycja.

Najważniejsze parametry to: normalize=True (zamiast liczebności zwraca udziały, czyli rozkład procentowy po pomnożeniu przez 100), sort=False (zachowuje kolejność wystąpień zamiast sortować), ascending=True (sortowanie rosnące) oraz bins=N (dla danych liczbowych ciągłych dzieli wartości na przedziały i zlicza je, działając jak szybki histogram tekstowy). Od pandas 1.1 metoda jest dostępna także na całym DataFrame — wtedy zlicza unikalne kombinacje wartości we wszystkich kolumnach (przydatne do szukania duplikatów wierszy lub częstych kombinacji cech).

Typowe zastosowania: ocena balansu klas w problemie klasyfikacji (czy zmienna celu nie jest skrajnie niezbalansowana), wykrywanie literówek i niespójności (np. „Polska", „polska", „PL", „ Polska " jako cztery różne kategorie zamiast jednej), znajdowanie wartości dominujących i rzadkich (kandydatów do połączenia w kategorię „inne"), a także szybkie liczenie braków w kolumnie. Warto pamiętać o pułapce: value_counts() zlicza dokładne wartości, więc spacje, wielkość liter i różne kodowania traktuje jako różne kategorie — dlatego często łączę go z .str.strip().str.lower() przed zliczeniem. Gdy interesuje mnie tylko liczba unikalnych wartości (a nie ich rozkład), szybsze jest nunique().

Przykład kodu:

import pandas as pd

# Podstawowe zliczenie — posortowane malejąco
df["kraj"].value_counts()

# Z brakami (NaN) jako osobną pozycją — kluczowe przy ocenie jakości
df["kraj"].value_counts(dropna=False)

# Udziały procentowe (rozkład) zamiast liczebności
df["kraj"].value_counts(normalize=True) * 100

# Dla danych liczbowych — podział na przedziały (mini-histogram)
df["wiek"].value_counts(bins=5).sort_index()

# Najpierw normalizacja tekstu, potem zliczanie — łączy "Polska" i " polska "
df["kraj"].str.strip().str.lower().value_counts()

# Zliczanie unikalnych KOMBINACJI wielu kolumn (od pandas 1.1)
df[["kraj", "miasto"]].value_counts()

# Tylko liczba unikalnych wartości (bez rozkładu)
df["kraj"].nunique()

Materiały

Jak szybko ocenić jakość zbioru danych — wykryć braki, wartości odstające i podstawowe statystyki?

Odpowiedź w 30 sekund: Idę stałą ścieżką: shape i info() (rozmiar, typy), isna().sum() (braki na kolumnę, najlepiej jako procent), describe(include='all') (statystyki, zakresy, kardynalność), value_counts() na kolumnach kategorycznych oraz duplicated().sum() (duplikaty). Wartości odstające wykrywam metodą IQR lub patrząc na rozjazd między medianą, średnią i wartościami min/max.

Odpowiedź w 2 minuty: Szybka ocena jakości danych to powtarzalny rytuał, który warto mieć „w palcach". Zaczynam od struktury: df.shape i df.info() mówią o rozmiarze, typach i liczbie wartości niepustych. Już tu często widać kolumny niemal całkowicie puste albo o złym typie.

Braki to drugi krok. df.isna().sum() daje liczbę braków na kolumnę, ale bardziej czytelny jest udział procentowy: df.isna().mean() * 100. Pozwala to od razu zdecydować, które kolumny nadają się do imputacji, a które do usunięcia. Warto też policzyć braki na wiersz (df.isna().sum(axis=1)), bo czasem problem dotyczy konkretnych rekordów, a nie kolumn. Równolegle sprawdzam duplikaty (df.duplicated().sum()), bo zdublowane wiersze zaburzają każdą statystykę.

Trzeci krok to statystyki opisowe: df.describe(include='all') pokazuje dla kolumn numerycznych średnią, odchylenie, min, kwartyle i max, a dla tekstowych liczbę unikalnych wartości oraz najczęstszą. Już sama różnica między średnią a medianą (kwartyl 50%) sygnalizuje skośność rozkładu, a nierealne wartości min/max (np. wiek 0 lub 999) zdradzają błędy lub wartości specjalne. Dla kolumn kategorycznych przeglądam value_counts(dropna=False), szukając literówek, niespójnego nazewnictwa i nieoczekiwanych kategorii.

Wartości odstające najczęściej wykrywam metodą IQR (rozstęp międzykwartylowy): liczę Q1 i Q3, a za odstające uznaję wartości poniżej Q1 − 1.5·IQR lub powyżej Q3 + 1.5·IQR. To podejście jest odporne na skrajne wartości i nie zakłada rozkładu normalnego (w przeciwieństwie do reguły 3 sigma). Dobrze uzupełnia je wizualizacja — boxplot dla pojedynczych kolumn i histogram dla kształtu rozkładu. Na koniec, dla wygody, często sięgam po gotowe narzędzia profilujące (np. ydata-profiling), które generują pełny raport jakości jednym poleceniem, ale fundamentem pozostaje znajomość powyższych metod „ręcznych".

Przykład kodu:

import pandas as pd

# 1) Struktura i typy
print(df.shape)
df.info()

# 2) Braki — liczba i udział procentowy na kolumnę
df.isna().sum()
(df.isna().mean() * 100).round(1).sort_values(ascending=False)

# Braki na wiersz oraz duplikaty
df.isna().sum(axis=1)
df.duplicated().sum()

# 3) Statystyki opisowe dla wszystkich kolumn
df.describe(include="all")

# 4) Rozkłady kolumn kategorycznych (z brakami)
for kol in df.select_dtypes(include=["object", "category"]).columns:
    print(df[kol].value_counts(dropna=False), "\n")

# 5) Wartości odstające metodą IQR (rozstęp międzykwartylowy)
def outliery_iqr(seria: pd.Series) -> pd.Series:
    q1, q3 = seria.quantile(0.25), seria.quantile(0.75)
    iqr = q3 - q1
    dolna, gorna = q1 - 1.5 * iqr, q3 + 1.5 * iqr
    return seria[(seria < dolna) | (seria > gorna)]

print(outliery_iqr(df["cena"]))

# Opcjonalnie: automatyczny raport jakości całego zbioru
# from ydata_profiling import ProfileReport
# ProfileReport(df, title="Raport jakości").to_file("raport.html")

Diagram:

flowchart TD
    A["Wczytaj dane (read_csv)"] --> B["Kształt i typy (shape, info)"]
    B --> C["Braki (isna().sum / mean)"]
    C --> D["Duplikaty (duplicated().sum)"]
    D --> E["Statystyki opisowe (describe(include='all'))"]
    E --> F["Rozkłady kategorii (value_counts)"]
    F --> G["Wartości odstające (IQR / boxplot)"]
    G --> H["Decyzje: czyszczenie, imputacja, transformacja"]

Materiały

Indeksowanie i selekcja danych

Jaka jest różnica między loc a iloc?

Odpowiedź w 30 sekund: loc wybiera dane po etykietach (nazwach indeksu i kolumn), natomiast iloc po pozycjach całkowitoliczbowych (0, 1, 2, ...). Kluczowa różnica praktyczna: wycinki w loc są domknięte na obu końcach (etykieta końcowa jest dołączana), a w iloc działają jak zwykłe slice'y Pythona — koniec jest wyłączony.

Odpowiedź w 2 minuty: loc i iloc to dwa podstawowe, jawne sposoby wyboru danych w pandas, które zastąpiły niejednoznaczne indeksowanie nawiasami df[...]. loc[wiersze, kolumny] operuje na etykietach — odwołujesz się do wartości indeksu i nazw kolumn. iloc[wiersze, kolumny] operuje na pozycjach — odwołujesz się do liczb porządkowych niezależnie od tego, jakie są rzeczywiste etykiety. Oba akcesory przyjmują pojedyncze wartości, listy, wycinki oraz maski boolowskie.

Najczęstsza pułapka to zachowanie wycinków. df.loc['a':'c'] zwróci wiersze o etykietach a, b oraz c — koniec jest włącznie. Natomiast df.iloc[0:3] zwróci wiersze na pozycjach 0, 1, 2 — koniec wyłącznie, dokładnie jak list[0:3]. To źródło wielu błędów off-by-one, gdy ktoś miesza oba style.

Druga ważna rzecz: jeśli indeks DataFrame to liczby całkowite (np. RangeIndex lub indeks po reset_index), loc[0:3] i iloc[0:3] mogą dać różne wyniki, bo loc potraktuje liczby jako etykiety (domknięte), a iloc jako pozycje. Gdy indeks jest niemonotoniczny albo nie zawiera danej etykiety, loc rzuci KeyError. Z kolei iloc rzuci IndexError przy pozycji poza zakresem. Dobrą praktyką jest świadomy wybór: gdy zależy ci na nazwach/etykietach (czytelność, odporność na zmianę kolejności) — używaj loc; gdy operujesz pozycyjnie (np. „pierwsze 5 wierszy", iteracja pozycyjna) — iloc.

Przykład kodu:

import pandas as pd

df = pd.DataFrame(
    {"imie": ["Ala", "Bartek", "Celina", "Darek"],
     "wiek": [23, 31, 45, 28]},
    index=["a", "b", "c", "d"],
)

# loc -> etykiety; wycinek DOMKNIĘTY (włącznie z 'c')
print(df.loc["a":"c", "imie"])
# a       Ala
# b    Bartek
# c    Celina

# iloc -> pozycje; wycinek WYŁĄCZA koniec (pozycje 0,1,2)
print(df.iloc[0:3, 0])
# a       Ala
# b    Bartek
# c    Celina

# Wybór konkretnej komórki
print(df.loc["b", "wiek"])   # 31  (etykieta wiersza, etykieta kolumny)
print(df.iloc[1, 1])         # 31  (pozycja wiersza, pozycja kolumny)

# Lista etykiet vs lista pozycji
print(df.loc[["a", "d"], ["wiek"]])   # wiersze a i d
print(df.iloc[[0, 3], [1]])           # te same wiersze, ale po pozycji

# loc przyjmuje też maskę boolowską (iloc nie przyjmuje serii boolowskiej z etykietami)
print(df.loc[df["wiek"] > 30])

Diagram:

flowchart TD
    A["Chcę wybrać dane z DataFrame"] --> B{"Odwołuję się przez etykietę czy pozycję?"}
    B -->|"Etykieta (nazwa indeksu / kolumny)"| C["df.loc[...]"]
    B -->|"Pozycja (liczba 0,1,2...)"| D["df.iloc[...]"]
    C --> E["Wycinek DOMKNIĘTY<br/>koniec włącznie<br/>brak etykiety -> KeyError"]
    D --> F["Wycinek jak w Pythonie<br/>koniec wyłączony<br/>pozycja poza zakresem -> IndexError"]

Materiały

Kiedy warto użyć at i iat zamiast loc i iloc?

Odpowiedź w 30 sekund: at i iat służą do dostępu do pojedynczej komórki (skalara) — at po etykietach, iat po pozycjach. Są zauważalnie szybsze od loc/iloc dla pojedynczej wartości, bo pomijają logikę obsługi wycinków, list i wyrównywania. Używaj ich w pętlach lub gorących ścieżkach, gdzie czytasz/zapisujesz jedną komórkę wiele razy.

Odpowiedź w 2 minuty: at i iat to wyspecjalizowane akcesory zaprojektowane wyłącznie do operacji na jednej komórce. at[etykieta_wiersza, etykieta_kolumny] jest odpowiednikiem loc ograniczonym do skalara, a iat[pozycja_wiersza, pozycja_kolumny] — odpowiednikiem iloc. Ponieważ nie muszą rozważać przypadków wycinków, list etykiet czy masek boolowskich, ich ścieżka kodu jest znacznie krótsza, co daje istotne przyspieszenie przy wielokrotnym dostępie do pojedynczych wartości (np. w pętli aktualizującej macierz wyników).

Najważniejsze ograniczenie: at/iat akceptują tylko skalarne etykiety/pozycje. Jeśli przekażesz listę, wycinek lub maskę, dostaniesz błąd. Innymi słowy, są to narzędzia „chirurgiczne" — do jednej komórki, nie do podzbiorów. Dla dowolnego wyboru wielu wierszy/kolumn nadal używaj loc/iloc.

W praktyce różnica wydajności staje się znacząca dopiero przy bardzo dużej liczbie pojedynczych dostępów. Jeśli iterujesz po wierszach DataFrame i odczytujesz/zapisujesz pojedyncze komórki, at/iat mogą być kilkukrotnie szybsze niż loc/iloc. Warto jednak pamiętać o szerszej zasadzie pandas: iteracja po wierszach jest sama w sobie wolna i często da się ją zastąpić operacjami zwektoryzowanymi. at/iat to optymalizacja dla przypadków, gdy wektoryzacja naprawdę nie jest możliwa (np. obliczenia rekurencyjne, gdzie kolejny wiersz zależy od poprzedniego). Typowa pułapka: używanie at tam, gdzie potrzebny jest wybór wielu wartości — wtedy trzeba wrócić do loc.

Przykład kodu:

import pandas as pd

df = pd.DataFrame(
    {"x": [10, 20, 30], "y": [1, 2, 3]},
    index=["a", "b", "c"],
)

# Odczyt pojedynczej komórki
print(df.at["b", "x"])    # 20  (po etykiecie)
print(df.iat[1, 0])       # 20  (po pozycji)

# Zapis pojedynczej komórki — szybki, skalarny
df.at["b", "x"] = 99
df.iat[2, 1] = 0

# BŁĄD: at/iat nie przyjmują list ani wycinków
# df.at["a", ["x", "y"]]   # -> błąd; do tego użyj loc
print(df.loc["a", ["x", "y"]])   # poprawnie dla wielu kolumn

# Typowe użycie: obliczenie zależne od poprzedniego wiersza (gdy nie da się zwektoryzować)
df["skum"] = 0
df.iat[0, df.columns.get_loc("skum")] = df.iat[0, 0]
for i in range(1, len(df)):
    df.iat[i, df.columns.get_loc("skum")] = (
        df.iat[i - 1, df.columns.get_loc("skum")] + df.iat[i, 0]
    )

Tabela porównawcza:

Cecha loc iloc at iat
Typ indeksowania etykieta pozycja etykieta pozycja
Skalar (jedna komórka) tak tak tylko skalar tylko skalar
Wycinek / lista / maska tak tak nie nie
Wybór wielu wierszy/kolumn tak tak nie nie
Szybkość dla 1 komórki wolniej wolniej najszybciej najszybciej
Typowe zastosowanie podzbiory po nazwie podzbiory po pozycji szybki dostęp do 1 komórki po etykiecie szybki dostęp do 1 komórki po pozycji

Materiały

Czym jest indeksowanie boolowskie (maska logiczna) i jak je stosować do filtrowania wierszy?

Odpowiedź w 30 sekund: Indeksowanie boolowskie polega na utworzeniu serii wartości True/False (maski logicznej) o tej samej długości co DataFrame i przekazaniu jej do df[maska] lub df.loc[maska]. Pandas zwraca tylko te wiersze, dla których maska ma wartość True. To podstawowy, zwektoryzowany sposób filtrowania danych po warunku.

Odpowiedź w 2 minuty: Maska logiczna powstaje, gdy zastosujesz operator porównania do kolumny, np. df["wiek"] > 30. Wynikiem jest Series typu bool z wartością True/False dla każdego wiersza. Przekazując tę maskę do DataFrame (df[maska] lub jaśniej df.loc[maska]), otrzymujesz podzbiór złożony wyłącznie z wierszy spełniających warunek. Mechanizm jest zwektoryzowany — działa na całych kolumnach naraz, bez jawnej pętli, dlatego jest szybki i czytelny.

Kluczowe jest wyrównanie po indeksie: pandas dopasowuje maskę do DataFrame po etykietach indeksu, a nie po pozycjach. Jeśli maska pochodzi z innego, niezsynchronizowanego obiektu, możesz dostać nieoczekiwane NaN albo IndexingError. Najbezpieczniej tworzyć maskę bezpośrednio z tego samego DataFrame, który filtrujesz. Maska musi mieć też właściwą długość — pomyłki tu kończą się błędem.

Do filtrowania wierszy używa się też metod pomocniczych zwracających maski: .isin([...]) (czy wartość należy do zbioru), .between(a, b) (zakres), .str.contains("wzorzec") (dopasowanie tekstu), .isna()/.notna() (braki danych). Maskę można negować operatorem ~ (np. ~df["aktywny"] to „nieaktywni"). Częsta pułapka: użycie słów kluczowych and/or zamiast operatorów &/| przy łączeniu warunków — Python próbuje wtedy ocenić całą serię jako pojedynczą wartość logiczną i rzuca ValueError: The truth value of a Series is ambiguous. Druga pułapka to filtrowanie z modyfikacją: df[df.a > 0]["b"] = 1 to chained indexing, które może nie zadziałać i wywoła SettingWithCopyWarning — zamiast tego użyj jednego wyrażenia df.loc[df.a > 0, "b"] = 1.

Przykład kodu:

import pandas as pd

df = pd.DataFrame({
    "imie": ["Ala", "Bartek", "Celina", "Darek", "Ewa"],
    "wiek": [23, 31, 45, 28, 39],
    "miasto": ["Wrocław", "Kraków", "Wrocław", "Gdańsk", "Kraków"],
})

# Maska logiczna: seria True/False
maska = df["wiek"] > 30
print(maska)
# 0    False
# 1     True
# 2     True
# 3    False
# 4     True

# Filtrowanie wierszy maską
print(df[maska])             # tylko wiersze, gdzie maska == True
print(df.loc[df["wiek"] > 30])   # to samo, zalecana forma jawna

# Metody zwracające maski
print(df[df["miasto"].isin(["Wrocław", "Gdańsk"])])  # należy do zbioru
print(df[df["wiek"].between(28, 39)])                # zakres (włącznie)
print(df[df["imie"].str.contains("a", case=False)])  # dopasowanie tekstu

# Negacja maski operatorem ~
print(df[~df["miasto"].isin(["Kraków"])])            # wszyscy spoza Krakowa

# POPRAWNE filtrowanie z przypisaniem (bez chained indexing)
df.loc[df["wiek"] > 40, "kategoria"] = "senior"

Materiały

Jak wybierać dane spełniające jednocześnie wiele warunków i dlaczego trzeba zachować ostrożność z operatorami &, |?

Odpowiedź w 30 sekund: Wiele warunków łączy się operatorami wektorowymi & (AND), | (OR) i ~ (NOT), a nie słowami and/or/not. Każdy pojedynczy warunek trzeba otoczyć nawiasami, bo & i | mają w Pythonie wyższy priorytet niż operatory porównania >, == itd. Brak nawiasów lub użycie and/or skutkuje błędem ValueError: The truth value of a Series is ambiguous.

Odpowiedź w 2 minuty: Aby wybrać wiersze spełniające kilka warunków naraz, łączysz maski logiczne operatorami bitowymi działającymi element po elemencie: & to logiczne AND, | to OR, a ~ to negacja. Na przykład „osoby starsze niż 30 lat z Krakowa" zapiszesz jako df[(df["wiek"] > 30) & (df["miasto"] == "Kraków")]. Operatory te działają na całych seriach boolowskich, zwracając nową maskę.

Pierwszy powód ostrożności to priorytet operatorów. W Pythonie & i | wiążą mocniej niż >, <, ==. Wyrażenie df["wiek"] > 30 & df["miasto"] == "Kraków" zostanie zinterpretowane jako df["wiek"] > (30 & df["miasto"]) == "Kraków", co prowadzi do błędu lub bezsensownego wyniku. Dlatego każdy warunek musi być w nawiasach: (df["wiek"] > 30) & (df["miasto"] == "Kraków"). To najczęstszy błąd początkujących i jeden z ulubionych „haczyków" rekruterów.

Drugi powód to różnica między operatorami wektorowymi (&, |, ~) a skalarnymi słowami kluczowymi (and, or, not). Słowa kluczowe wymagają, by operand dało się ocenić jako pojedynczą wartość True/False. Seria pandas zawiera wiele wartości, więc Python nie wie, czy „seria jest prawdziwa" — i rzuca ValueError: The truth value of a Series is ambiguous. Operatory &/| działają natomiast element po elemencie, dlatego są jedyną poprawną opcją.

Dla wielu warunków na tej samej kolumnie wygodniejsze bywają metody: .isin([...]) zamiast łańcucha (col == a) | (col == b) | ..., czy .between(low, high) zamiast (col >= low) & (col <= high). Przy bardzo rozbudowanych warunkach czytelniejsza może być metoda query() (np. df.query("wiek > 30 and miasto == 'Kraków'")), gdzie wewnątrz łańcucha używa się naturalnych and/or. Warto też pamiętać o NaN: braki danych w porównaniach dają False, co może niepostrzeżenie usunąć wiersze — wtedy łączy się warunek z .notna().

Przykład kodu:

import pandas as pd

df = pd.DataFrame({
    "imie": ["Ala", "Bartek", "Celina", "Darek", "Ewa"],
    "wiek": [23, 31, 45, 28, 39],
    "miasto": ["Wrocław", "Kraków", "Wrocław", "Gdańsk", "Kraków"],
})

# POPRAWNIE: nawiasy wokół KAŻDEGO warunku, operator wektorowy &
wynik = df[(df["wiek"] > 30) & (df["miasto"] == "Kraków")]
print(wynik)
#     imie  wiek  miasto
# 1 Bartek    31  Kraków
# 4    Ewa    39  Kraków

# OR (|) i negacja (~)
print(df[(df["wiek"] < 25) | (df["miasto"] == "Gdańsk")])
print(df[~(df["miasto"] == "Wrocław")])

# BŁĄD 1: brak nawiasów -> & ma wyższy priorytet niż >
# df[df["wiek"] > 30 & df["miasto"] == "Kraków"]   # ValueError

# BŁĄD 2: użycie and/or zamiast &/|
# df[(df["wiek"] > 30) and (df["miasto"] == "Kraków")]  # ValueError: ambiguous

# Czytelniejsze alternatywy
print(df[df["miasto"].isin(["Kraków", "Gdańsk"])])     # zamiast (==)|(==)
print(df[df["wiek"].between(28, 39)])                  # zamiast (>=)&(<=)

# query() pozwala używać naturalnych and/or wewnątrz łańcucha
print(df.query("wiek > 30 and miasto == 'Kraków'"))

Diagram:

flowchart TD
    A["Łączę kilka warunków na DataFrame"] --> B{"Jakiego operatora użyć?"}
    B -->|"AND"| C["& (NIE 'and')"]
    B -->|"OR"| D["| (NIE 'or')"]
    B -->|"NOT"| E["~ (NIE 'not')"]
    C --> F["Otocz KAŻDY warunek nawiasami<br/>(df.a > 1) & (df.b == 2)"]
    D --> F
    E --> F
    F --> G{"Brak nawiasów lub and/or?"}
    G -->|"Tak"| H["ValueError:<br/>truth value of a Series is ambiguous"]
    G -->|"Nie"| I["Poprawna maska -> filtrowanie OK"]

Materiały

Czym jest MultiIndex (indeks hierarchiczny) i w jakich scenariuszach jest przydatny?

Odpowiedź w 30 sekund: MultiIndex to wielopoziomowy indeks hierarchiczny, który pozwala każdej osi (wierszom lub kolumnom) mieć więcej niż jeden poziom etykiet — np. (region, miasto) albo (rok, kwartał). Umożliwia reprezentowanie danych wielowymiarowych w dwuwymiarowym DataFrame oraz wygodne grupowanie, agregację i selekcję po poszczególnych poziomach.

Odpowiedź w 2 minuty: MultiIndex (zwany też indeksem hierarchicznym) zastępuje pojedynczy poziom etykiet zestawem kilku poziomów ułożonych hierarchicznie. Dzięki temu wiersz może być identyfikowany krotką etykiet, np. ("Południe", "Kraków"). Tworzy się go najczęściej przez set_index([kol1, kol2]), przez groupby([kol1, kol2]).agg(...) (wynik ma MultiIndex z kluczy grupowania) albo metodami fabrycznymi pd.MultiIndex.from_tuples/from_product/from_arrays. Kolumny również mogą mieć MultiIndex — typowo po operacjach pivot_table czy unstack.

Główne scenariusze użycia to: dane panelowe i szeregi czasowe z wieloma wymiarami (np. metryka × data × kraj), wyniki grupowania po wielu kluczach, dane z naturalną hierarchią (kraj → województwo → miasto), oraz przekształcenia long↔wide (stack/unstack, pivot). MultiIndex pozwala kompaktowo trzymać to, co inaczej wymagałoby tablicy wielowymiarowej, zachowując dwuwymiarowy interfejs DataFrame.

Selekcja odbywa się przez loc z krotkami etykiet: df.loc[("Południe", "Kraków")] wybiera konkretną kombinację, a df.loc["Południe"] — cały blok najwyższego poziomu. Do bardziej elastycznych przekrojów służy pd.IndexSlice (alias idx), np. df.loc[idx[:, "Kraków"], :], oraz metoda xs(klucz, level=...) do przekroju po wybranym poziomie. Pomocne są też swaplevel, reorder_levels, droplevel i reset_index.

Najważniejsze pułapki: (1) wydajna i poprawna selekcja wycinkami wymaga posortowanego indeksu — jeśli nie jest, pandas zgłosi UnsortedIndexError lub ostrzeże o wydajności; rozwiązaniem jest df.sort_index(). (2) Selekcja częściowa bywa myląca — df.loc["Kraków"] zadziała tylko dla pierwszego poziomu; dla głębszych poziomów potrzebny jest IndexSlice lub xs. (3) MultiIndex zwiększa złożoność kodu i bywa trudniejszy w utrzymaniu; gdy hierarchia nie jest naprawdę potrzebna, czasem prościej trzymać dane „płasko" w zwykłych kolumnach i grupować na bieżąco.

Przykład kodu:

import pandas as pd

df = pd.DataFrame({
    "region": ["Południe", "Południe", "Północ", "Północ"],
    "miasto": ["Kraków", "Katowice", "Gdańsk", "Gdynia"],
    "rok": [2023, 2023, 2023, 2023],
    "sprzedaz": [100, 80, 120, 60],
})

# Utworzenie MultiIndex z dwóch kolumn
mi = df.set_index(["region", "miasto"])
print(mi)
#                    rok  sprzedaz
# region   miasto
# Południe Kraków   2023       100
#          Katowice 2023        80
# Północ   Gdańsk   2023       120
#          Gdynia   2023        60

# WAŻNE: posortuj indeks przed selekcją wycinkami
mi = mi.sort_index()

# Wybór całego bloku najwyższego poziomu
print(mi.loc["Południe"])

# Wybór konkretnej kombinacji (krotka)
print(mi.loc[("Południe", "Kraków")])

# Przekrój po głębszym poziomie — IndexSlice lub xs
idx = pd.IndexSlice
print(mi.loc[idx[:, "Gdańsk"], :])      # wszystkie regiony, miasto Gdańsk
print(mi.xs("Gdańsk", level="miasto"))  # to samo przez xs

# Operacje na poziomach
print(mi.swaplevel().sort_index())      # zamiana kolejności poziomów
print(mi.reset_index())                 # spłaszczenie z powrotem do kolumn

# MultiIndex powstaje naturalnie z groupby po wielu kluczach
print(df.groupby(["region", "rok"])["sprzedaz"].sum())

Diagram:

flowchart TD
    MI["MultiIndex (indeks wierszy)"] --> P0["Poziom 0: region"]
    P0 --> R1["Południe"]
    P0 --> R2["Północ"]
    R1 --> C1["Kraków"]
    R1 --> C2["Katowice"]
    R2 --> C3["Gdańsk"]
    R2 --> C4["Gdynia"]
    C1 --> V1["wiersz: rok, sprzedaz"]
    C2 --> V2["wiersz: rok, sprzedaz"]
    C3 --> V3["wiersz: rok, sprzedaz"]
    C4 --> V4["wiersz: rok, sprzedaz"]

Materiały

Do czego służy metoda query i jakie ma zalety oraz ograniczenia w porównaniu z klasycznym indeksowaniem?

Odpowiedź w 30 sekund: query() filtruje wiersze na podstawie wyrażenia zapisanego jako tekst w naturalnej składni, np. df.query("wiek > 30 and miasto == 'Kraków'"). Zaletą jest czytelność (brak powtarzania nazwy df, użycie and/or) i potencjalna wydajność dla dużych danych dzięki silnikowi numexpr. Ograniczenia to konieczność uważnego cytowania tekstu, słabsze wsparcie narzędzi/IDE i kłopotliwe nazwy kolumn ze spacjami.

Odpowiedź w 2 minuty: DataFrame.query() przyjmuje łańcuch znaków z wyrażeniem boolowskim i zwraca przefiltrowany DataFrame. Wewnątrz wyrażenia odwołujesz się do kolumn po nazwie bez prefiksu df[...], używasz naturalnych operatorów logicznych and/or/not (zamiast &/|/~), porównań łańcuchowych w stylu Pythona (18 < wiek < 65) oraz operatora in/not in. Do zmiennych z otaczającego zakresu odnosisz się przez @nazwa, np. df.query("wiek > @prog").

Główne zalety: (1) Czytelność — wyrażenie jest zwięzłe, bez wielokrotnego df[...] i bez plątaniny nawiasów wymaganych przy &/|; warunki złożone czyta się prawie jak zdanie. (2) Wydajność dla dużych danych — dla odpowiednio dużych ramek query() (i eval()) mogą używać silnika numexpr, który ocenia wyrażenia bez tworzenia kosztownych tablic pośrednich i potrafi wykorzystać wiele rdzeni, co bywa szybsze i oszczędniejsze pamięciowo niż klasyczne maski. (3) Mniej miejsca na błąd &/| — nie trzeba pamiętać o priorytetach operatorów ani o nawiasach wokół każdego warunku.

Ograniczenia i pułapki: (1) Wyrażenie to tekst, więc IDE nie podpowie nazw kolumn ani nie wychwyci literówek — błędy ujawniają się dopiero w czasie wykonania. (2) Cytowanie bywa kłopotliwe: wartości tekstowe wymagają cudzysłowów wewnątrz łańcucha ("miasto == 'Kraków'"), co przy zmiennych staje się niewygodne. (3) Nazwy kolumn ze spacjami lub znakami specjalnymi wymagają otoczenia backtickami ("`liczba osób` > 3"). (4) Dla małych ramek query() bywa wolniejsze od zwykłej maski z powodu narzutu parsowania wyrażenia. (5) Mniejsza elastyczność — nie wszystkie operacje (np. wywołania dowolnych metod na kolumnach) są wspierane tak swobodnie jak w zwykłym kodzie; bywa, że trzeba ustawić engine="python". (6) Konstruowanie zapytań przez sklejanie stringów z danych wejściowych grozi „wstrzyknięciem" — bezpieczniej używać @zmienna. W praktyce: query() świetnie sprawdza się przy złożonych, czytelnych filtrach i dużych zbiorach; do prostych warunków oraz tam, gdzie zależy ci na podpowiedziach IDE i pełnej elastyczności Pythona, lepsze pozostaje klasyczne indeksowanie boolowskie z loc.

Przykład kodu:

import pandas as pd

df = pd.DataFrame({
    "imie": ["Ala", "Bartek", "Celina", "Darek", "Ewa"],
    "wiek": [23, 31, 45, 28, 39],
    "miasto": ["Wrocław", "Kraków", "Wrocław", "Gdańsk", "Kraków"],
})

# query: naturalna składnia, and/or, bez powtarzania df[...]
print(df.query("wiek > 30 and miasto == 'Kraków'"))

# Porównanie łańcuchowe i operator in
print(df.query("28 <= wiek <= 39"))
print(df.query("miasto in ['Kraków', 'Gdańsk']"))

# Odwołanie do zmiennej z zewnątrz przez @ (bezpieczniejsze niż sklejanie stringów)
prog = 30
print(df.query("wiek > @prog"))

# Nazwy kolumn ze spacją -> backticki
df2 = df.rename(columns={"miasto": "miasto zamieszkania"})
print(df2.query("`miasto zamieszkania` == 'Kraków'"))

# Równoważne klasyczne indeksowanie boolowskie (czytelne wsparcie IDE, pełna elastyczność)
print(df[(df["wiek"] > 30) & (df["miasto"] == "Kraków")])

Tabela porównawcza:

Cecha query() Klasyczne indeksowanie boolowskie
Składnia tekst, naturalne and/or maski + &/`
Czytelność złożonych warunków wysoka niższa (dużo nawiasów)
Podpowiedzi / sprawdzanie w IDE brak (to string) pełne
Wydajność (duże dane) często szybsze (numexpr) dobre, lecz tworzy tablice pośrednie
Wydajność (małe dane) narzut parsowania zwykle szybsze
Kolumny ze spacją wymaga backticków zwykła indeksacja df["a b"]
Zmienne zewnętrzne przez @nazwa bezpośrednio w kodzie
Elastyczność (dowolne metody) ograniczona pełna

Materiały

Czyszczenie danych i braki

21. Jak pandas reprezentuje wartości brakujące (NaN, None, NaT, pd.NA) i czym się one różnią?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
22. Jak wykrywać i usuwać braki danych (isna, notna, dropna)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
23. Jakie strategie uzupełniania braków oferuje fillna (wartość stała, ffill/bfill, interpolacja) i kiedy którą stosować?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
24. Jak wykrywać i usuwać duplikaty (duplicated, drop_duplicates)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
25. Jak zmieniać nazwy kolumn oraz etykiet indeksu (rename, przypisanie do columns)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Transformacja danych

26. Czym różnią się apply, map i applymap i kiedy użyłbyś której z tych metod?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
27. Dlaczego wektoryzacja jest preferowana nad apply oraz jawnymi pętlami w pandas?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
28. Jak tworzyć nowe kolumny na podstawie istniejących i jaką przewagę daje metoda assign?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
29. Do czego służą where, mask oraz np.select przy warunkowym przypisywaniu wartości?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
30. Jak bezpiecznie konwertować typy danych (astype, to_numeric, convert_dtypes)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Grupowanie i agregacja

31. Jak działa groupby i na czym polega model „split-apply-combine"?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
32. Jak zastosować wiele funkcji agregujących jednocześnie i nazwać wynikowe kolumny (agg, named aggregation)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
33. Czym różni się transform od agg w kontekście groupby i kiedy użyć którego?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
34. Czym jest pivot_table i czym różni się od pivot?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
35. Do czego służy crosstab i jak różni się od pivot_table?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Łączenie i scalanie danych

36. Jaka jest różnica między merge, join i concat?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
37. Jakie typy złączeń (how: inner, left, right, outer) obsługuje merge i czym się różnią?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
38. Jak łączyć ramki danych po indeksie, a jak po wspólnych kolumnach kluczowych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
39. Jakie problemy mogą wystąpić przy łączeniu danych (duplikaty kluczy, niezgodne typy, eksplozja liczby wierszy)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Reshaping i struktura danych

40. Czym różni się format „szeroki" (wide) od „długiego" (long) i kiedy stosować każdy z nich?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
41. Do czego służą melt i pivot oraz jak się wzajemnie uzupełniają?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
42. Jak działają stack i unstack w kontekście indeksu hierarchicznego?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
43. Jak rozbijać i rozwijać kolumny złożone (str.split, explode, akcesor .str)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Szeregi czasowe

44. Jak pandas reprezentuje daty i czas (Timestamp, DatetimeIndex, Period, Timedelta)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
45. Jak konwertować kolumny na typ daty (to_datetime) i obsługiwać strefy czasowe oraz różne formaty?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
46. Czym jest resample i czym różni się od groupby przy danych czasowych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
47. Do czego służą okna kroczące (rolling, expanding) i jak liczyć np. średnią ruchomą?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Wydajność, pamięć i typowe pułapki

48. Czym jest SettingWithCopyWarning, jaka jest jego przyczyna i jak go uniknąć?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
49. Czym różni się widok (view) od kopii (copy) w pandas i dlaczego ma to znaczenie przy modyfikacji danych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
50. Jak ograniczać zużycie pamięci ramki danych (dobór dtype, typ category, downcast)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
51. Dlaczego iterrows i apply bywają wolne i jakie są szybsze alternatywy (wektoryzacja, eval, query)?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
52. Co robi parametr inplace i dlaczego jego użycie bywa odradzane?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
53. Kiedy pandas przestaje być właściwym narzędziem i warto sięgnąć po Dask, Polars lub bazę danych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 33 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Chcesz poznać wszystkie odpowiedzi?

Uzyskaj pełny dostęp do 53 pytań rekrutacyjnych z pandas oraz pozostałych technologii.

Zobacz plany cenowe