Założenia analizy ankiety i punkt kontrolny „czy to w ogóle ma sens?”
Cel badania jako punkt wyjścia do całego workflow
Analiza ankiety w Pythonie ma sens tylko wtedy, gdy wiadomo, na jakie pytanie decyzyjne odpowiada. Najpierw trzeba określić, co ma się zmienić po zobaczeniu wyników: jaka decyzja, jakie działanie, jaka rekomendacja. Bez tego nawet perfekcyjny kod i piękne wykresy stają się wyłącznie ćwiczeniem technicznym.
Cel badania powinien mieć minimum trzech cech: być konkretny (np. „porównanie poziomu satysfakcji klientów między kanałami sprzedaży”), powiązany z decyzją („czy inwestować w rozwój kanału online”), oraz mierzalny na bazie tego, co faktycznie zmierzono w ankiecie (pytania, skale, filtracje). Jeśli pytania ankietowe nie wspierają celu, workflow analizy będzie sztucznie rozciągany, a interpretacje – naciągane.
Dla kontroli jakości warto spisać krótki opis wyniku końcowego w dwóch–trzech zdaniach, np.: „Celem analizy jest porównanie średniej oceny satysfakcji (skala 1–5) między klientami korzystającymi z kanału A i B, z kontrolą wieku oraz płci. Wynik ma wesprzeć decyzję o alokacji budżetu marketingowego.” Jeśli nie da się tego zrobić jasno, to sygnał ostrzegawczy: brakuje definicji celu lub dane go nie obsłużą.
Jeśli cel jest rozmyty, raporty będą pełne przypadkowych tabelek i wykresów bez wyraźnej linii argumentacji. Jeśli cel jest skondensowany i zapisany z wyprzedzeniem, łatwiej ułożyć workflow analizy w Pythonie jako serię punktów kontrolnych prowadzących do tego jednego wyniku końcowego (lub kilku jasno określonych).
Typy pytań ankietowych i ich konsekwencje dla analizy
Kwestionariusz ankiety jest zlepkiem różnych typów pytań, a każda forma odpowiedzi niesie konkretne konsekwencje dla dalszej analizy w Pythonie. Z punktu widzenia workflow kluczowe jest rozpoznanie tych typów i przypisanie ich do odpowiednich struktur danych oraz metod statystycznych.
Najczęściej pojawiają się:
- pytania jednokrotnego wyboru – odpowiedź nominalna (np. „płeć”, „kanał zakupu”); w Pythonie to zwykle kolumny typu category lub object, używane w tabelach krzyżowych i prostych statystykach opisowych;
- pytania wielokrotnego wyboru – lista opcji z możliwością zaznaczenia kilku; często eksportowane jako jedna kolumna z rozdzielonymi wartościami (np. „A;C;D”) albo jako kilka kolumn binarnych (0/1); wymagają osobnej obróbki;
- skale Likerta (np. 1–5, „zdecydowanie się nie zgadzam” – „zdecydowanie się zgadzam”) – dane porządkowe; w praktyce często traktowane jako ilościowe (liczenie średniej), ale dla testów statystycznych lepiej rozważyć metody dla danych porządkowych;
- pytania otwarte – tekstowe odpowiedzi, wymagające kodowania jakościowego albo prostych analiz tekstu (tokenizacja, wyszukiwanie słów kluczowych, proste grupowanie).
W workflow analizy ankiety w Pythonie typ pytania determinuje wybór dalszych kroków: inne transformacje stosuje się do skal Likerta (mapowanie na wartości numeryczne, porządkowanie), inne do danych nominalnych (tworzenie pd.Categorical), a jeszcze inne do odpowiedzi otwartych (czyszczenie tekstu, ewentualne kodowanie ręczne).
Jeśli struktura pytań jest jasna i dobrze odwzorowana w typach danych pandas, można bezpiecznie planować testy statystyczne i wizualizacje. Jeśli typy pytań są pomieszane (np. skala zapisana jako tekst bez jasnego porządku), każdy krok analizy będzie wymagał dodatkowych napraw, a ryzyko błędnej interpretacji rośnie.
Błędy konstrukcji pytań i ich wpływ na analitykę
Nawet najlepszy kod w Pythonie nie skompensuje słabego kwestionariusza. Dwa szczególnie problematyczne obszary to niejednoznaczne pytania oraz brak odpowiednich kategorii odpowiedzi. Przykładem są pytania łączące kilka wątków („Jak oceniasz szybkość i jakość obsługi?”), które generują odpowiedzi trudne do interpretacji – nie wiadomo, co dokładnie ocenia respondent.
Drugi typ błędów to niekompletny zestaw odpowiedzi, bez kategorii „nie dotyczy”, „trudno powiedzieć”, „nie pamiętam”. Respondenci zmuszani są do wyboru odpowiedzi nieadekwatnej, a później w analizie trudno rozdzielić brak wiedzy od np. neutralnej postawy. W danych pojawia się szum, który może zaburzyć średnie i rozkłady.
Kolejna pułapka to źle zdefiniowane skale (np. różna liczba stopni dla podobnych pytań, brak jednoznacznego opisu krańców skali). W Pythonie da się te skale przeskalować lub przemapować, ale ich semantyka zostaje obciążona błędem konstrukcyjnym – wyniki z takiej skali są trudne do uczciwej obrony.
Jeśli w kwestionariuszu widać pytania wieloznaczne, brakujące opcje odpowiedzi, niespójne skale, to sygnał ostrzegawczy: analiza będzie podatna na nadinterpretacje. Jeśli pytania są krótkie, jednoznaczne i wszystkie skale opisane w podobny sposób, dane można traktować jako solidny fundament dla dalszych etapów workflow w Pythonie.
Ryzykowne sytuacje: gdy analiza ankiety może nie mieć sensu
Zanim zacznie się pisać kod, trzeba ocenić, czy zebrane dane w ogóle nadają się do analizy, która ma znaczenie decyzyjne. Kilka mocnych sygnałów ostrzegawczych pojawia się regularnie:
- zbyt mała próba – np. kilkadziesiąt odpowiedzi przy próbie wyciągania wniosków o całej populacji klientów; Python policzy wszystko, ale rozkłady będą niestabilne, a testy statystyczne – mało wiarygodne;
- brak metadanych o ankiecie – brak informacji o sposobie doboru próby, kanale rekrutacji, czasie trwania badania; utrudnia to ocenę reprezentatywności i zasięgu wniosków;
- brak jednolitego kodowania odpowiedzi – różne kody dla tych samych kategorii (np. „K”, „F” i „kobieta” w tym samym pytaniu), mieszanie wartości liczbowych i tekstowych;
- brak jasnej jednostki analizy – nie wiadomo, czy rekord w danych to jedna osoba, jedno zamówienie, czy jedno wypełnienie (np. ktoś mógł wypełnić ankietę kilka razy).
Jeżeli co najmniej dwa z powyższych sygnałów są obecne, potrzeba ostrożnej rewizji planu analizy. Być może dane nadają się tylko do wewnętrznych, orientacyjnych wniosków, a nie do raportowania „na zewnątrz”. Python może wygenerować dokładne liczby, ale problem leży w tym, co one tak naprawdę opisują.
Jeśli próba jest wystarczająca, jednostka analizy jest jednoznaczna (np. „jeden wiersz = jeden respondent”), a kodowanie odpowiedzi da się spójnie opisać, workflow ma solidne podstawy i warto przejść do przygotowania środowiska oraz struktury projektu.

Przygotowanie środowiska i struktury projektu analitycznego w Pythonie
Minimalny stack technologiczny do analizy ankiety
Do rzetelnej analizy ankiety w Pythonie wystarczy niewielki, ale dobrze dobrany zestaw bibliotek. Rdzeniem jest pandas (manipulacja danymi), wspierany przez numpy (operacje numeryczne) oraz matplotlib/seaborn (wizualizacje). Do testów statystycznych przydaje się scipy oraz ewentualnie statsmodels i pingouin.
Przykładowa instalacja środowiska (np. w wirtualnym środowisku):
pip install pandas numpy matplotlib seaborn scipy statsmodels pingouin
W wielu projektach ankietowych nie ma potrzeby sięgać po rozbudowane narzędzia typu scikit-learn, chyba że planowane są segmentacje, proste modele predykcyjne lub klasyfikacje odpowiedzi. Minimalny, stabilny stack ma tę zaletę, że kod jest czytelny, łatwy do zreprodukowania i prosty do uruchomienia na innych maszynach.
Jeśli biblioteki są zainstalowane w sposób kontrolowany (np. w pliku requirements.txt), łatwo odtworzyć dokładne wersje środowiska, co jest kluczowe przy audycie powtarzalności wyników. Jeśli brak pliku z zależnościami i wszystko instalowane jest „ręcznie”, odtworzenie wyników po kilku miesiącach staje się znacznie trudniejsze.
Struktura katalogów jako element kontroli jakości
Chaotyczna struktura plików to jeden z głównych praktycznych problemów w analizie ankiet. Dobrze zdefiniowany układ katalogów pełni rolę systemu punktów kontrolnych, który wymusza porządek i ułatwia audyt. Minimalny, sprawdzony wzorzec to:
data_raw/– surowe dane z narzędzia ankietowego, nieedytowane;data_processed/– dane po wstępnym czyszczeniu i standaryzacji;notebooks/– notatniki Jupyter z eksploracją, prototypowaniem kodu i analizami;scripts/– sparametryzowane skrypty Pythona, które można uruchamiać automatycznie;reports/– wygenerowane raporty, wykresy, tabele w formatach dla interesariuszy;config/– pliki konfiguracyjne, np. słownik pytań, ustawienia ścieżek.
Taki układ od razu pokazuje, na którym etapie jest dany plik i jakie ma znaczenie. Surowe dane są nienaruszalne, wszystkie modyfikacje są odzwierciedlone w plikach pośrednich w data_processed/, a raporty nie mieszają się z kodem. Przy pracy zespołowej struktura katalogów jest jednym z głównych elementów szybko widocznej higieny projektu.
Jeśli pliki są porozrzucane („final.csv”, „final_new.csv”, „final_real_last.xlsx”), ryzyko pomylenia wersji danych i generowania wyników z nieodpowiedniej próbki jest bardzo wysokie. Jeśli katalogi są zdefiniowane, a nazwy plików sensowne (z datą, wersją), łatwo ustalić, jak powstał każdy wynik i na jakich danych się opiera.
Kontrola wersji kodu i danych
Do powtarzalnej analizy ankiety w Pythonie konieczny jest choćby minimalny system kontroli wersji. Git jest standardem, ale nawet prosty schemat z notatnikiem i datami jest lepszy niż nic. Kluczowe elementy to:
- repozytorium kodu (skrypty, notatniki, pliki konfiguracyjne, dokumentacja),
- opisane w
README.mdźródła danych, formaty plików i główne etapy workflow, - oznaczanie kamieni milowych (np. tagi w gicie) odpowiadających kolejnym wersjom raportów.
Surowe dane z ankiety zwykle są zbyt duże lub wrażliwe, by przechowywać je w repozytorium git. Lepiej umieścić w README opis lokalizacji, numeru wersji, daty eksportu oraz ewentualnej ścieżki na serwerze czy w S3. Dla danych przetworzonych dobrym kompromisem jest trzymanie plików CSV o rozsądnej wielkości w repozytorium lub w dobrze udokumentowanym miejscu.
Jeśli kod i opis wersji są spójne, można łatwo odtworzyć konkretne wyniki, co jest jednym z kluczowych kryteriów przy audycie jakości analizy. Jeśli wersjonowanie jest pomijane, każde odtworzenie wyników wymaga ręcznego dochodzenia, jakie ustawienia i pliki były użyte – co znacząco zwiększa ryzyko błędu.
Globalne ustawienia i spójność środowiska
Na początku projektu warto zdefiniować kilka globalnych ustawień, aby uniknąć drobnych, ale powtarzających się problemów:
- ustawienia wyświetlania pandas (liczba wierszy/kolumn, format liczb),
- ustawienie ziarna losowego (
np.random.seed()) przy losowaniach, - domyślne style wykresów,
- domyślny katalog roboczy, z którego wczytywane są dane.
Przykładowy plik config/settings.py:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)
pd.set_option("display.max_rows", 100)
pd.set_option("display.max_columns", 50)
sns.set(style="whitegrid", context="talk")
Taki plik można importować na początku każdego notatnika lub skryptu. Ujednolica to wygląd wyników i eliminuje sytuacje, w których różne osoby pracujące nad tym samym projektem mają inne ustawienia środowiska, prowadzące do subtelnych rozbieżności.
Jeśli środowisko jest poskładane z przypadkowych ustawień i zależności, odtworzenie analizy po czasie jest skomplikowane. Jeśli istnieje centralny plik konfiguracji i spis użytych bibliotek, cały workflow staje się transparentny i łatwiejszy do audytu.
Import danych ankietowych i pierwsza inspekcja jakości
Formaty eksportu ankiet i typowe pułapki
Narzędzia ankietowe (Google Forms, SurveyMonkey, LimeSurvey, Qualtrics i inne) eksportują dane najczęściej do plików CSV lub XLSX. Na tym etapie pojawia się szereg specyficznych pułapek, które mają bezpośredni wpływ na analizę w Pythonie:
- niestandardowe nagłówki (np. pierwszy wiersz z metadanymi, a dopiero kolejny z nazwami pytań),
- długie etykiety w nagłówkach, zawierające pełny tekst pytania, często ze znakami specjalnymi,
Specyficzne konstrukcje pytań a struktura danych
Eksport z narzędzia ankietowego rzadko jest prostą tabelą „pytanie – odpowiedź”. Sposób, w jaki pytania zostały skonfigurowane, bezpośrednio przekłada się na kształt danych. Przy pierwszej inspekcji trzeba zidentyfikować kilka typowych konstrukcji, bo od tego zależy dalszy workflow w Pythonie.
- pytania jednokrotnego wyboru – zwykle pojedyncza kolumna z jedną wartością (kod lub etykieta);
- pytania wielokrotnego wyboru – często rozwinięte na wiele kolumn typu „P5_A”, „P5_B”, „P5_C” z wartościami 0/1, „tak/nie” lub „checked/unchecked”;
- skale (Likerta, zgody, ocen) – kolumny z wartościami liczbowymi lub tekstowymi („zdecydowanie się zgadzam” itp.), czasem z wymieszanymi typami;
- pytania macierzowe – kombinacja skali i wielu pod-pytań, rozbita na szereg kolumn; bez słownika pytań interpretacja bywa niejednoznaczna;
- pytania otwarte – pola tekstowe o bardzo zróżnicowanej długości i zawartości, często z literówkami, emotikonami, URL-ami.
Jeśli typ konstrukcji nie jest jasno rozpoznany przy importowaniu danych, późniejsze agregacje i testy statystyczne będą wykonywane na przypadkowych kombinacjach kolumn. Jeżeli natomiast od razu oznaczysz, które grupy kolumn należą do tego samego pytania, możesz przejść do spójnego czyszczenia.
Robocze mapowanie pytań na kolumny
Punktem kontrolnym na tym etapie jest stworzenie roboczej mapy łączącej nazwy kolumn z identyfikatorami pytań oraz typem odpowiedzi. Nawet prosta tabela w CSV znacząco redukuje ryzyko pomyłek.
Załóżmy, że plik eksportu znajduje się w data_raw/survey_export.xlsx:
import pandas as pd
from pathlib import Path
raw_path = Path("data_raw") / "survey_export.xlsx"
df_raw = pd.read_excel(raw_path)
# Podgląd nagłówków
df_raw.columns.tolist()[:20]
Na podstawie listy nagłówków można ręcznie (lub półautomatycznie) wygenerować słownik pytań:
question_map = pd.DataFrame({
"column_name": df_raw.columns
})
# Przykładowa adnotacja ręczna lub półautomatyczna
question_map["question_id"] = question_map["column_name"].str.extract(r"(Pd+)", expand=False)
question_map["question_type"] = None # do uzupełnienia
question_map["description"] = None # uproszczony opis pytania
Taką tabelę dobrze jest zapisać do config/question_map.csv i uzupełnić ręcznie tam, gdzie automatyka zawodzi. Jest to minimalny standard dokumentacji struktury ankiety w projekcie analitycznym.
Jeżeli mapowanie kolumn do pytań powstaje na bieżąco „w głowie” analityka, rośnie ryzyko nieświadomego pomijania części zmiennych lub łączenia niepowiązanych kolumn. Jeżeli istnieje jawna tabela mapująca, każdą grupę kolumn można audytować i odtworzyć logikę analizy.
Import z nienormatywnymi nagłówkami
Eksporty z wielu narzędzi zaczynają się od kilku wierszy metadanych (np. data eksportu, liczba pytań), a nazwy kolumn z pytaniami pojawiają się dopiero w dalszej części pliku. To typowy sygnał ostrzegawczy – wczytanie takiego pliku „domyślnie” jako CSV lub XLSX powoduje potraktowanie metadanych jako wiersza z odpowiedziami.
Bezpieczny schemat to:
- otworzyć plik ręcznie i policzyć, który wiersz zawiera pierwsze prawdziwe nagłówki,
- wczytać dane z parametrem
header=<indeks wiersza>, - osobno zapisać metadane (np. do pliku tekstowego w
config/).
# Przykład dla CSV z dwoma pierwszymi wierszami jako metadane
df_raw = pd.read_csv("data_raw/export.csv", header=2)
# Ewentualne zachowanie metadanych
meta = pd.read_csv("data_raw/export.csv", nrows=2, header=None)
meta.to_csv("config/export_metadata.csv", index=False)
Jeśli wiersz z nagłówkami zostanie źle zidentyfikowany, każdy dalszy krok analizy będzie oparty na błędnym założeniu co do znaczenia kolumn. Jeśli już na starcie zapiszesz metadane i jednoznacznie wybierzesz wiersz z nagłówkami, minimalizujesz ryzyko cichego przesunięcia struktury danych.
Normalizacja nazw kolumn i przygotowanie słownika pytań
Nazwy kolumn z eksportu bywają długie, zawierają znaki specjalne, spacje, a czasem całe zdania. Taki format jest niewygodny w kodzie, utrudnia filtrowanie i grupowanie. W praktyce stosuje się dwupoziomowe podejście:
- w danych roboczych stosunkowo krótkie, techniczne identyfikatory (np.
Q1_gender), - w słowniku pytań pełne treści i etykiety przyjazne odbiorcom raportu.
Przykładowa funkcja do „oczyszczenia” nagłówków:
import re
def clean_column_name(col):
col = col.strip()
col = col.lower()
col = re.sub(r"[^0-9a-zA-ZżźćńółęąśŻŹĆĄŚĘŁÓŃ]+", "_", col)
col = re.sub(r"_+", "_", col)
col = col.strip("_")
return col
df = df_raw.copy()
df.columns = [clean_column_name(c) for c in df_raw.columns]
Równolegle można zbudować słownik:
question_dict = pd.DataFrame({
"raw_name": df_raw.columns,
"clean_name": df.columns,
"question_id": question_map["question_id"],
"type": question_map["question_type"],
"description": question_map["description"],
})
question_dict.to_csv("config/question_dict.csv", index=False)
Jeśli wszystkie operacje będą wykonywane na surowych, długich nagłówkach, szybko pojawią się literówki i niekonsekwencje w kodzie. Jeśli od razu zostanie zdefiniowana przejrzysta warstwa „technicznych” nazw kolumn i słownika pytań, kolejne etapy analizy są prostsze do audytu i utrzymania.
Identyfikacja duplikatów i wielokrotnych wypełnień
Jedną z pierwszych kontroli jakości po imporcie jest sprawdzenie, czy w danych nie występują duplikaty rekordów lub wielokrotne wypełnienia przez tych samych respondentów. Niezależnie od wyboru strategii (usuwanie, agregacja, ważenie), trzeba najpierw jasno zdiagnozować problem.
Typowe źródła identyfikacji respondenta:
- unikalny identyfikator nadany przez narzędzie ankietowe (np.
ResponseId), - adres e-mail lub inny login (w badaniach nieanonimowych),
- kombinacja pól takich jak data wypełnienia + IP + kluczowe odpowiedzi.
# Załóżmy, że mamy kolumnę response_id
df["is_duplicate_id"] = df.duplicated(subset=["response_id"], keep=False)
dupes = df[df["is_duplicate_id"]].sort_values("response_id")
dupes[["response_id"]].value_counts().head()
Jeżeli brakuje stabilnego identyfikatora, można próbować heurystyk:
subset_cols = ["email", "q1_gender", "q2_age"] # przykład
df["possible_duplicate"] = df.duplicated(subset=subset_cols, keep=False)
Jeśli analiza duplikatów pominie się na starcie, kolejne etapy będą traktowały wielokrotne wypełnienia jako niezależne obserwacje – sztucznie zawyżając częstość pewnych odpowiedzi. Jeżeli przynajmniej w przybliżeniu zidentyfikujesz naddane odpowiedzi, możesz świadomie zdecydować, czy łączyć je, czy odrzucać.
Wstępne filtrowanie rekordów niekompletnych i testowych
Eksporty z narzędzi ankietowych często zawierają:
- odpowiedzi „testowe” – wypełniane przez zespół projektowy,
- rekordy przerwane – respondent nie doszedł do końca ankiety,
- rekordy techniczne – np. puste wiersze, zapisy systemowe.
Dobrą praktyką jest stworzenie kolumny statusu (np. record_status), która opisuje, czy dany wiersz jest brany pod uwagę w analizie głównej.
df["record_status"] = "candidate"
# Przykład: odfiltrowanie pustych lub prawie pustych rekordów
min_answered = 3 # minimum niepustych odpowiedzi
non_na_counts = df.notna().sum(axis=1)
df.loc[non_na_counts < min_answered, "record_status"] = "drop_too_many_missing"
# Przykład: oznaczenie rekordów testowych po e-mailu lub znaczniku narzędzia
test_emails = ["test@example.com", "qa@firma.pl"]
df.loc[df["email"].isin(test_emails), "record_status"] = "drop_test"
Dopiero po zdefiniowaniu statusów można zbudować ramkę roboczą:
df_work = df[df["record_status"] == "candidate"].copy()
Jeśli filtrowanie rekordów odbywa się „ad hoc” na różnych etapach, trudno później ustalić, dlaczego liczba respondentów zmniejsza się między notatnikami. Jeżeli powstaje jawna kolumna ze statusem i zdefiniowany jest próg minimum odpowiedzi, każdą decyzję o wykluczeniu można prześledzić.
Standaryzacja kodowania wartości odpowiedzi
Kolejnym punktem kontrolnym jest ujednolicenie sposobu kodowania odpowiedzi w całym zbiorze. Ten etap w praktyce decyduje o tym, czy dalsze analizy w Pythonie będą stabilne i zrozumiałe.
Typowe problemy:
- mieszanie wartości liczbowych i tekstowych w tej samej kolumnie (np.
1,"1","zdecydowanie się zgadzam"), - różne kody dla tej samej kategorii („K”, „kobieta”, „female”),
- różne reprezentacje braków danych (puste pola, „brak danych”, „N/D”, „-999”).
Podstawowe podejście to budowa słowników mapowania dla każdej kluczowej zmiennej kategorycznej. Przykład dla pytania o płeć:
gender_map = {
"K": "female",
"kobieta": "female",
"F": "female",
"M": "male",
"mężczyzna": "male",
"male": "male",
}
df_work["gender_std"] = (
df_work["q1_gender"]
.str.strip()
.str.lower()
.replace(gender_map)
)
# Standaryzacja braków
missing_like = ["brak danych", "n/d", "nie wiem", "no answer"]
df_work["gender_std"] = df_work["gender_std"].replace(missing_like, pd.NA)
Analogicznie można potraktować skale ocen, np. 1–5, 1–7, „zdecydowanie się zgadzam” itp., zapisując mapy w osobnych plikach w config/:
likert_5_map = {
"zdecydowanie się nie zgadzam": 1,
"raczej się nie zgadzam": 2,
"ani się zgadzam, ani się nie zgadzam": 3,
"raczej się zgadzam": 4,
"zdecydowanie się zgadzam": 5,
}
Jeżeli standaryzacja wartości będzie robiona „na bieżąco” w środku notatników, każda osoba w zespole może zdefiniować inne mapowania. Jeżeli mapy są trwałymi artefaktami w config/, spójność kodowania da się łatwo skontrolować i poprawić.
Obsługa pytań wielokrotnego wyboru i macierzowych
Pytania wielokrotnego wyboru i macierzowe stanowią osobną kategorię ryzyka, ponieważ ich struktura w danych jest z natury wielokolumnowa. Brak wspólnej konwencji przetwarzania prowadzi do niespójnych analiz.
Przykład: pytanie o kanały kontaktu, gdzie respondent mógł zaznaczyć wiele odpowiedzi, a eksport generuje kolumny:
q5_email,q5_phone,q5_chat.
Podstawowe strategie:
- reprezentacja „wide” – pozostawienie kolumn 0/1 w szerokim formacie,
- reprezentacja „long” – przekształcenie do formatu „odpowiedź – kanał – zaznaczone/niezaznaczone”,
- lista kategorii – zbudowanie jednej kolumny zawierającej listę zaznaczonych odpowiedzi.
Przykład przekształcenia do formatu „long” dla pytań wielokrotnego wyboru:
multi_cols = ["q5_email", "q5_phone", "q5_chat"]
df_multi = (
df_work[["response_id"] + multi_cols]
.melt(id_vars="response_id",
value_vars=multi_cols,
var_name="channel",
value_name="checked")
)
# Przy założeniu, że checked to 1/0 lub True/False
df_multi = df_multi[df_multi["checked"] == 1]
# Normalizacja nazw kanałów
df_multi["channel"] = df_multi["channel"].str.replace("q5_", "")
Analogicznie można potraktować pytania macierzowe (np. ocena różnych aspektów usługi w skali 1–5), rozbijając każdą pod-zmienną na osobny wiersz i przechowując identyfikator aspektu i oceny. To ułatwia późniejsze porównania i agregacje.
Tworzenie zmiennych analitycznych i wskaźników kompozytowych
Surowe odpowiedzi ankietowe rzadko są bezpośrednio użyteczne analitycznie. Zwykle potrzebne są zmienne pochodne – wskaźniki, sumy, średnie lub flagi logiczne, które syntetyzują kilka pól w jedną mierzalną cechę. To moment, w którym dane zaczynają „mówić” o zjawisku, a nie o pojedynczych kliknięciach w ankiecie.
Typowe przykłady zmiennych analitycznych:
- wskaźniki satysfakcji (np. średnia z kilku pytań o usługę),
- indeksy lojalności (np. połączenie NPS z deklarowaną częstotliwością zakupu),
- segmenty zdefiniowane na podstawie kilku odpowiedzi (np. heavy user / light user),
- flagi logiczne (np. „wymaga follow-upu”, „ryzyko odejścia wysokie”).
Prosty przykład: indeks satysfakcji klienta jako średnia z trzech pytań w skali 1–5:
sat_cols = ["q10_speed", "q11_quality", "q12_support"]
# Kryterium minimum: respondent musi mieć co najmniej 2 z 3 odpowiedzi
df_work["sat_non_na"] = df_work[sat_cols].notna().sum(axis=1)
df_work["sat_index"] = (
df_work[sat_cols].mean(axis=1)
.where(df_work["sat_non_na"] >= 2) # inaczej NaN
)
# Dokumentacja w słowniku pytań / wskaźników
derived_vars = pd.DataFrame([{
"clean_name": "sat_index",
"question_id": "IDX_SAT",
"type": "derived_continuous",
"description": "Średnia z q10_speed, q11_quality, q12_support; min 2 odpowiedzi"
}])
Każdy taki wskaźnik powinien mieć:
- jawnie zapisane źródłowe kolumny,
- definicję progu minimum odpowiedzi,
- informację, czy wyższa wartość oznacza lepszy czy gorszy wynik,
- opis w
config/derived_vars.csvlub w innym źródle konfiguracji.
Jeżeli wskaźniki kompozytowe powstają „ręcznie” w wielu notatnikach, po kilku iteracjach projekt traci spójność: ten sam indeks jest liczony różnie w zależności od osoby i daty. Jeśli wszystkie zmienne pochodne są zdefiniowane w jednym, wersjonowanym miejscu, każdy kolejny krok analizy ma stabilną podstawę i łatwą ścieżkę audytu.
Walidacja wewnętrzna: spójność odpowiedzi
Nawet po oczyszczeniu struktury danych, sam kontent odpowiedzi wymaga kontroli. Chodzi o wykrycie zestawów odpowiedzi, które są ze sobą logicznie sprzeczne lub ekstremalnie mało prawdopodobne.
Przykładowe reguły spójności:
- wiek a status zawodowy (np. 12 lat i „emeryt” – sygnał ostrzegawczy),
- czas wypełnienia ankiety a długość kwestionariusza (komplet w 30 sekund – podejrzane),
- sprzeczne deklaracje w pytaniach warunkowych (np. „nie korzystam z produktu”, a dalej szczegółowa ocena jego funkcji).
Przykład prostej walidacji wieku i statusu:
# Załóżmy, że mamy q2_age (liczba) oraz q3_status (kategoria)
age = df_work["q2_age"].astype("Int64")
df_work["flag_inconsistent_age_status"] = False
df_work.loc[
(age < 16) & df_work["q3_status"].isin(["pracuje na pełen etat", "emeryt"]),
"flag_inconsistent_age_status"
] = True
Przykład kontroli czasu wypełnienia, jeśli mamy znaczniki czasowe:
# Załóżmy, że mamy start_time i end_time jako datetime
df_work["duration_sec"] = (
df_work["end_time"] - df_work["start_time"]
).dt.total_seconds()
min_reasonable = 60 # minimalnie sensowny czas na przejście ankiety
df_work["flag_too_fast"] = df_work["duration_sec"] < min_reasonable
Takie flagi nie muszą automatycznie usuwać respondentów. Mogą pełnić rolę punktu kontrolnego: jeśli odsetek rekordów z flagą przekracza rozsądne minimum, wymagana jest decyzja metodologiczna, a nie automatyczne czyszczenie.
Jeżeli zbudujesz zestaw reguł spójności i będziesz je uruchamiać po każdym imporcie, każdy kolejny plik z wynikami przejdzie ten sam audyt. Jeśli te reguły będą ukryte w pojedynczych notatkach, analiza w czasie stanie się nieporównywalna, a źródeł niespójności trudno będzie dochodzić.
Opisowy przegląd danych: rozkłady i brakujące odpowiedzi
Po zbudowaniu warstwy technicznej danych trzeba wykonać przegląd opisowy: rozkłady odpowiedzi, poziom braków, podstawowe statystyki. To etap, na którym szybko wychodzą na jaw błędy w kodowaniu, problemy z ankietą i niereprezentatywność.
Prosta funkcja do opisu zmiennych kategorycznych:
def describe_categorical(df, col, normalize=True, dropna=False):
counts = df[col].value_counts(dropna=dropna)
if normalize:
perc = counts / counts.sum()
desc = pd.DataFrame({
"count": counts,
"perc": perc.round(3)
})
else:
desc = pd.DataFrame({"count": counts})
return desc
desc_gender = describe_categorical(df_work, "gender_std")
print(desc_gender.head())
Dla zmiennych ciągłych (jak wiek, indeksy, czas wypełnienia) użyteczny jest zestaw podstawowych statystyk z dodatkowym licznikiem braków:
def describe_numeric(df, col):
s = df[col]
desc = s.describe(percentiles=[0.1, 0.25, 0.5, 0.75, 0.9])
missing = s.isna().sum()
return pd.DataFrame(desc).assign(missing=missing)
desc_age = describe_numeric(df_work, "q2_age")
print(desc_age)
Przy przeglądzie rozkładów warto od razu wprowadzić prostą wizualizację, aby wychwycić artefakty:
import seaborn as sns
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6, 4))
sns.histplot(df_work["q2_age"], bins=20, kde=False, ax=ax)
ax.set_title("Rozkład wieku respondentów")
plt.tight_layout()
Jeśli histogram wieku ma „piki” na podejrzanych wartościach (np. 999, 0 lub tylko równe dziesiątki), to sygnał ostrzegawczy co do jakości pomiaru. Jeżeli rozkłady kategorii pokazują jedną dominującą odpowiedź, trzeba wrócić do treści pytania – może konstrukcja pytania była niejednoznaczna albo części odpowiedzi nie była poprawnie oferowana w narzędziu.
Systematyczna analiza braków danych
Braki danych w ankietach są regułą, nie wyjątkiem. Kluczowe jest, czy są one losowe, czy też skorelowane z cechami respondentów. To różnica między lekkim pogorszeniem precyzji a poważnym zniekształceniem wniosków.
Pierwszy krok to obliczenie udziału braków dla każdej zmiennej:
missing_summary = (
df_work.isna()
.mean()
.rename("missing_rate")
.reset_index()
.rename(columns={"index": "column"})
)
missing_summary.sort_values("missing_rate", ascending=False).head(20)
Dobrą praktyką jest ustalenie progu, powyżej którego kolumna jest kandydataem do wyłączenia z analiz głównych (np. 40–50% braków). To nie jest automatyczna reguła, ale punkt kontrolny, który wymaga świadomej decyzji.
Kolejny krok to analiza braków w zależności od kluczowych cech, np. płci, wieku, kanału pozyskania respondenta:
def missing_by_group(df, col, group_col):
tmp = df.copy()
tmp["missing"] = tmp[col].isna()
res = (
tmp.groupby(group_col)["missing"]
.mean()
.rename("missing_rate")
.reset_index()
)
return res
miss_income_by_gender = missing_by_group(df_work, "q20_income", "gender_std")
print(miss_income_by_gender)
Jeżeli stopa braków w pytaniu o dochód jest znacząco wyższa w jednej grupie (np. wśród młodszych badanych), to sygnał ostrzegawczy przed bezrefleksyjnym używaniem tej zmiennej do porównań międz grupami. Taka obserwacja powinna zostać odnotowana w raporcie metodologicznym, nie tylko w notatniku z kodem.
Jeśli nie przeprowadzisz osobnej analizy braków, każda późniejsza tabela i wykres będą niejawnie filtrować respondentów, którzy nie odpowiedzieli na dane pytanie. W praktyce porównasz różne podzbiory, choć etykiety tabel będą sugerowały, że dotyczą „całej próby”.
Ważenie wyników: korekta struktury próby
Rzeczywista struktura respondentów prawie nigdy idealnie nie odwzorowuje populacji. Jeżeli projekt wymaga wnioskowania dla populacji (np. wszystkich klientów, mieszkańców miasta), często potrzebne jest ważenie wyników według znanych parametrów zewnętrznych (wiek, płeć, region).
Minimalny zestaw kroków przy ważeniu:
- zdefiniowanie docelowej struktury populacji (np. z danych GUS lub CRM),
- zbudowanie tabeli rozkładów w próbie dla tych samych kategorii,
- obliczenie wag jako relacji udziału w populacji do udziału w próbie,
- przypisanie wagi każdemu respondentowi.
Przykład prostego ważenia jednowymiarowego po płci:
# Struktura populacji (np. zewnętrzne źródło)
pop_gender = pd.DataFrame({
"gender_std": ["female", "male"],
"pop_share": [0.52, 0.48]
})
# Struktura próby
sample_gender = (
df_work["gender_std"]
.value_counts(normalize=True)
.rename("sample_share")
.reset_index()
.rename(columns={"index": "gender_std"})
)
weights = pop_gender.merge(sample_gender, on="gender_std", how="left")
weights["weight"] = weights["pop_share"] / weights["sample_share"]
# Dołączenie wag do danych respondentów
df_work = df_work.merge(weights[["gender_std", "weight"]], on="gender_std", how="left")
Każdy respondent z brakującą wagą (np. nie podał płci) powinien być jawnie oznaczony, np. osobną wagą weight_missing_gender lub wartością NaN. Automatyczne podstawianie wagi 1 dla takich rekordów zaburza interpretację.
Przy ważeniu wielowymiarowym (np. płeć × wiek × region) stosuje się algorytmy iteracyjne (raking / Rim weighting). W Pythonie można to zaimplementować ręcznie lub skorzystać z dedykowanych bibliotek, jednak kluczowy jest audyt: czy rozkłady po ważeniu są wystarczająco zgodne z celem i czy wagi nie przyjmują ekstremalnych wartości.
# Szybki audyt rozkładu wag
df_work["weight"].describe()
df_work["weight"].hist(bins=30)
Jeżeli część wag jest bardzo wysoka (np. powyżej ustalonego progu), to sygnał ostrzegawczy, że struktura próby znacząco odbiega od populacji i korekta jest agresywna. W takim przypadku należy rozważyć korekty projektu (dobór próby) lub ograniczyć zakres wnioskowania, zamiast maskować problem ekstremalnymi wagami.
Segmentacja respondentów: definicje i etykiety
W praktyce raportowania rzadko pokazuje się wyniki dla „całej próby” bez podziału. Wymagane są segmenty: grupy różniące się zachowaniem, postawami, wartością biznesową. Z punktu widzenia audytu kluczowe jest, aby definicje segmentów były jednoznaczne i odtwarzalne.
Przykład prostego podziału na segmenty lojalności na podstawie NPS i częstotliwości zakupu:
# Załóżmy, że mamy zmienne: nps_score (-100..100) i freq_purchase (1..7)
def assign_loyalty_segment(row):
if pd.isna(row["nps_score"]) or pd.isna(row["freq_purchase"]):
return pd.NA
if row["nps_score"] >= 50 and row["freq_purchase"] >= 5:
return "lojalni_promotorzy"
if row["nps_score"] < 0 and row["freq_purchase"] <= 3:
return "ryzyko_odpływu"
return "neutralni"
df_work["seg_loyalty"] = df_work.apply(assign_loyalty_segment, axis=1)
Definicje segmentów warto utrzymywać w jednym miejscu, np. jako funkcje lub reguły zapisane w pliku konfiguracyjnym. Dobrą praktyką jest też zbudowanie słownika etykiet segmentów przyjaznych komunikacyjnie:
seg_labels = {
"lojalni_promotorzy": "Lojalni promotorzy (wysoki NPS, wysoka częstotliwość)",
"ryzyko_odpływu": "Ryzyko odpływu (niski NPS, niska częstotliwość)",
"neutralni": "Grupa neutralna / umiarkowanie zaangażowana",
}
df_work["seg_loyalty_label"] = df_work["seg_loyalty"].map(seg_labels)
Jeżeli segmenty są definiowane ad hoc w notebookach, każdy analityk może użyć trochę innych progów, co uniemożliwia porównanie wyników między falami badania. Jeśli segmentacja jest formalnie zdefiniowana i wersjonowana, każda zmiana jest świadomą decyzją metodologiczną, a nie przypadkowym skutkiem modyfikacji kodu.
Tabele krzyżowe i podstawowe testy statystyczne
Po przygotowaniu zmiennych i segmentów najczęstszym narzędziem pracy stają się tabele krzyżowe oraz podstawowe testy istotności. Chodzi o to, żeby od razu połączyć opis procentów ze sprawdzeniem, czy różnice mogą wynikać z przypadku.
Przykład: odsetek „zadowolonych” w segmentach lojalności. Najpierw definiujemy zmienną binarną:
Najczęściej zadawane pytania (FAQ)
Jak zacząć analizę ankiety w Pythonie, żeby nie marnować czasu na „ładne wykresy bez sensu”?
Punkt startowy to zawsze pytanie decyzyjne: co ma się zmienić po zobaczeniu wyników ankiety. Minimum to krótki opis celu w 2–3 zdaniach, powiązany z konkretną decyzją (np. alokacja budżetu, zmiana procesu obsługi) i oparty na realnie zebranych pytaniach i skalach.
Przed uruchomieniem Pythona zrób punkt kontrolny: sprawdź, czy potrafisz jednym zdaniem powiedzieć, jakiej liczby lub porównania szukasz (np. „różnica średniej satysfakcji między kanałami A i B z kontrolą wieku”). Jeśli nie, analiza zamieni się w przegląd losowych tabelek – sygnał ostrzegawczy, że trzeba doprecyzować cel albo przyciąć zakres pytań.
Jakie typy pytań ankietowych muszę rozpoznać przed analizą w pandas?
Kluczowe typy to: pytania jednokrotnego wyboru (nominalne), wielokrotnego wyboru, skale Likerta (porządkowe) oraz pytania otwarte (tekstowe). Każdy z nich powinien trafić do innej „szufladki” w kodzie: category/object dla nominalnych, kolumny binarne lub listy dla wielokrotnego wyboru, uporządkowane kategorie lub liczby dla Likerta, stringi dla tekstów.
Lista kontrolna przed analizą w pandas wygląda tak: czy każda kolumna ma poprawny typ (category/int/str), czy skale Likerta mają jasno zdefiniowaną kolejność, czy pola wielokrotnego wyboru są rozbite na sensowne kolumny. Jeśli typy są pomieszane (np. skala jako tekst bez kolejności), to sygnał ostrzegawczy: trzeba najpierw naprawić strukturę danych, inaczej testy i wykresy będą zdradliwie „poprawne”, ale źle zinterpretowane.
Jak w Pythonie obchodzić się ze skalą Likerta: traktować ją jako liczbową czy porządkową?
W praktyce skale Likerta (np. 1–5, „zdecydowanie się nie zgadzam” – „zdecydowanie się zgadzam”) często są traktowane jak dane ilościowe: liczy się średnią, odchylenie, robi proste wykresy. Technicznie są to dane porządkowe, więc do testów statystycznych sensowniejsze bywają metody dla rang (np. testy nieparametryczne), a nie klasyczne modele dla danych ciągłych.
Punkt kontrolny: jeśli chcesz raportować „średnią z Likerta”, upewnij się, że skala jest symetryczna, opisana na krańcach i ma spójną liczbę stopni w całym kwestionariuszu. Jeśli skale są niespójne albo opisy krańców różne, traktowanie wyników jak liczb ciągłych to mocny sygnał ostrzegawczy – lepiej użyć rozkładów odpowiedzi i mediany niż sztywnej średniej.
Jak rozpoznać, że moja ankieta jest zbyt słaba, żeby robić na niej „poważną” analizę w Pythonie?
Typowe sygnały ostrzegawcze to: zbyt mała liczebność próby (kilkadziesiąt osób przy ambicji uogólniania na całą populację), brak informacji o sposobie doboru respondentów, brak jasnej jednostki analizy (nie wiadomo, czy wiersz to osoba, zamówienie czy wypełnienie) oraz chaos w kodowaniu odpowiedzi (różne kody dla tej samej kategorii).
Jeśli widzisz przynajmniej dwa z tych problemów jednocześnie, traktuj analizę wyłącznie orientacyjnie, do użytku wewnętrznego. Punkt kontrolny jest prosty: jeśli nie potrafisz uczciwie opisać, „kogo” reprezentuje każdy wiersz i „jak” respondenci trafili do badania, Python dostarczy jedynie dokładnych liczb o niejasnym znaczeniu.
Jakie błędy konstrukcji pytań najbardziej psują wyniki analizy ankiety?
Najmocniej szkodzą pytania łączące kilka wątków („Jak oceniasz szybkość i jakość obsługi?”), brakujące odpowiedzi typu „nie dotyczy”/„trudno powiedzieć” oraz niespójne skale (różna liczba stopni, inne opisy krańców). Kod w Pythonie da się dopasować do niemal każdej formy, ale nie usunie błędu logicznego w samych pytaniach.
Punkt kontrolny przed głęboką analizą: przejrzyj pytania i sprawdź, czy każde dotyczy jednego zagadnienia, czy każdy respondent miał możliwość uczciwej odpowiedzi (w tym „nie wiem”), a skale są porównywalne między pytaniami. Jeśli widzisz wieloznaczne pytania i brakujące kategorie odpowiedzi, to sygnał, że interpretacje będą łatwe do podważenia – analiza może służyć jedynie jako jakościowy „trop”, a nie twardy dowód.
Jaki minimalny zestaw bibliotek w Pythonie wystarczy do rzetelnej analizy ankiety?
Przy standardowej analizie ankiet wystarczy stabilny, mały stack: pandas do manipulacji danymi, numpy do obliczeń, matplotlib i seaborn do wykresów oraz scipy (ewentualnie statsmodels i pingouin) do testów statystycznych. Większość typowych zadań – od tabel krzyżowych po proste testy różnic – da się zrealizować w tym zestawie.
Dobry punkt kontrolny to plik z zależnościami (np. requirements.txt) z konkretnymi wersjami bibliotek. Jeśli instalujesz wszystko „z ręki” i nie zapisujesz wersji, odtworzenie wyników za kilka miesięcy będzie trudne lub niemożliwe. Stabilny stack i spięte wersje to minimum, jeśli analiza ma być audytowalna i powtarzalna.
Jak w Pythonie poradzić sobie z pytaniami wielokrotnego wyboru w ankiecie?
Pytania wielokrotnego wyboru najczęściej pojawiają się jako jedna kolumna z wartościami rozdzielonymi separatorem (np. „A;C;D”) lub jako kilka kolumn binarnych 0/1. W pierwszym przypadku potrzebna jest transformacja: rozbicie stringa na listę i rozwinięcie jej do formatu „jedna kategoria na kolumnę” albo „jedna odpowiedź na wiersz”. W drugim – ujednolicenie nazewnictwa kolumn i jasne opisanie, co oznacza 0 i 1.
Lista kontrolna: sprawdź, jak dokładnie eksportuje je system ankietowy, czy separator jest jednolity, czy nie ma literówek w kodach odpowiedzi i czy dokumentacja wyjaśnia znaczenie każdej kolumny. Jeśli pytania wielokrotnego wyboru są wymieszane z innymi typami w jednej kolumnie lub różnie kodowane w plikach, to wyraźny sygnał ostrzegawczy – zanim ruszysz z analizą, trzeba zbudować spójny słownik kodów i przemapować dane.
Bibliografia i źródła
- Survey Methodology. Wiley (2004) – Podstawy projektowania ankiet, doboru próby i oceny jakości danych
- Design, Evaluation, and Analysis of Questionnaires for Survey Research. Wiley (2008) – Konstrukcja pytań, błędy pomiaru, interpretacja odpowiedzi
- Likert R. A Technique for the Measurement of Attitudes. Archives of Psychology (1932) – Oryginalny opis skali Likerta i jej własności pomiarowych
- Standards for Educational and Psychological Testing. American Educational Research Association (2014) – Standardy trafności, rzetelności i interpretacji wyników pomiaru
- European Social Survey (ESS) – Methodology Overview. European Social Survey ERIC – Praktyki jakościowe w badaniach ankietowych, dobór próby i kwestionariusz
- Survey Research Methods. SAGE Publications (2014) – Przegląd metod badań ankietowych, konstrukcja pytań i analiza danych
- Python for Data Analysis. O’Reilly Media (2022) – Praktyczne użycie pandas i numpy w analizie danych, w tym danych tabelarycznych







Bardzo podoba mi się, jak w artykule omówiono cały proces analizy danych zebranych z ankiety przy użyciu Pythona. Szczegółowo opisane kroki oraz zaprezentowany kod sprawiają, że nawet osoby początkujące w analizie danych mogą z łatwością przejść przez cały proces. Natomiast jako sugestię mam, że brakuje mi trochę więcej głębszej analizy wyników ankiety – np. interpretacji zależności między poszczególnymi pytaniami czy przeprowadzenia bardziej zaawansowanych testów statystycznych. Byłoby to uzupełnienie doskonałego artykułu o jeszcze bardziej zaawansowany poziom analizy danych.
Komentarze są aktywne tylko po zalogowaniu.