Baza wiedzy / Python
pandas to_json: eksport rekordów, dat, braków i indeksu
Eksport kończę kontrolą tego, co odbiorca rzeczywiście odczyta. Pokazuję kompletne przykłady zapisu tekstu i pliku, braków, Unicode, dat oraz JSON Lines.

to_json zapisuje obiekt pandas w formacie JSON. Najczęściej wywołasz df.to_json(...) na tabeli DataFrame. Ja traktuję eksport jako uzgodnienie z odbiorcą: co oznacza jeden rekord, gdzie zapisujemy identyfikator, jak przedstawiamy daty i co znaczy brak wartości. Samo utworzenie pliku nie dowodzi, że druga strona odczyta dane zgodnie z naszą intencją.
Poniższe przykłady są samodzielne i korzystają z danych syntetycznych. Wykonałem je w Pythonie 3.14.4, pandas 3.0.6 i NumPy 2.5.4. Pliki powstają tylko w katalogach tymczasowych. Porównuję także dane po ponownym odczycie, ponieważ poprawny składniowo JSON może utracić indeks, zmienić typ albo zapisać datę z niewłaściwą precyzją. Każdy z tych efektów wymaga osobnego sprawdzenia.
Bez ścieżki otrzymujesz tekst JSON
Metoda należy do DataFrame lub Series; nie wywołujemy jej jako pd.to_json. Jeżeli nie podasz path_or_buf, otrzymasz napis. Możesz go przekazać dalej do innej funkcji, zapisać albo sparsować w celu sprawdzenia. W przykładzie używam listy rekordów i ograniczam tabelę do dwóch jasno nazwanych kolumn.
# example: records
import json
import pandas as pd
df = pd.DataFrame({"id": ["001", "002"], "units": [0, 3]})
text = df.to_json(orient="records")
print(text)
print(json.loads(text) == [{"id": "001", "units": 0}, {"id": "002", "units": 3}])
Tekst to [{"id":"001","units":0},{"id":"002","units":3}], a porównanie zwraca True. Identyfikatory są napisami, a liczby pozostają liczbami. json.loads sprawdza strukturę po parsowaniu, więc test nie zależy od dodatkowych spacji czy wcięć. Takie porównanie jest często użyteczniejsze niż sprawdzanie, czy cały plik ma dokładnie identyczną reprezentację tekstową.
Napis JSON i obiekt Pythona to różne rzeczy. Po to_json nie wybierzesz rekordu przez indeksowanie napisu tak, jak indeksujesz listę słowników. Jeśli biblioteka HTTP oczekuje obiektu do samodzielnej serializacji, przekazanie jej gotowego JSON może spowodować podwójne kodowanie. Sprawdź interfejs odbiorcy i zdecyduj, kto odpowiada za zamianę struktury na tekst.
Orient wybieram według potrzeb odbiorcy
Dla DataFrame domyślne orient to columns. Zewnętrzne klucze oznaczają wtedy kolumny, a wewnętrzne indeks. records daje listę obiektów wierszy, wygodną w wielu umowach wymiany. split rozdziela indeks, nazwy kolumn i dane na osobne pola. Żaden z tych formatów nie jest uniwersalnie najlepszy: odbiorca musi znać wybraną reprezentację.
| Orient | Reprezentacja tabeli | Co trzeba uzgodnić |
|---|---|---|
records |
lista obiektów wierszy | indeks nie jest zapisany |
columns |
kolumna, potem indeks | etykiety jako klucze obiektu |
index |
indeks, potem pola | unikalność etykiet wierszy |
split |
index, columns, data |
zgodność kolejności list |
values |
tablica wartości | nazwy i indeks są poza plikiem |
table |
schema oraz data |
obsługa schematu przez odbiorcę |
Dokumentacja to_json opisuje dopuszczalne orientacje i ograniczenia indeksu. Formaty używające etykiet jako kluczy wymagają uwagi przy powtórzeniach. Obiekt JSON nie jest dobrym miejscem do reprezentowania kilku różnych rekordów pod tym samym kluczem. Jeśli indeks nie jest identyfikatorem, rozważ jawną kolumnę identyfikującą rekord.
Format values bywa krótki, ale bez nazw odbiorca musi znać kolejność kolumn z innego źródła. Przestawienie dwóch kolumn może wtedy zmienić znaczenie danych bez błędu parsera. W eksporcie służącym do współpracy wolę format, którego kontrakt da się sprawdzić na małej próbce. Oszczędność kilku znaków nie rekompensuje niejawnej interpretacji pól.
Indeks z identyfikatorem przenoszę do kolumny
orient="records" nie zapisuje etykiet indeksu. Jeśli indeks jest zwykłym licznikiem wierszy, może to być oczekiwane. Jeżeli przechowuje klucz sprawy albo produktu, utrata indeksu oznacza utratę ważnej informacji. Nazwij indeks i przenieś go do kolumny przed eksportem. Wtedy identyfikator stanie się zwykłym polem każdego rekordu.
# example: index
import json
import pandas as pd
df = pd.DataFrame({"units": [0, 3]}, index=pd.Index(["A", "B"], name="record_id"))
without_key = json.loads(df.to_json(orient="records"))
with_key = json.loads(df.reset_index().to_json(orient="records"))
print(without_key)
print(with_key)
Pierwszy wynik to [{'units': 0}, {'units': 3}]. Drugi zawiera dodatkowo record_id o wartościach A i B. Sprawdź wcześniej, czy tabela nie ma już kolumny o nazwie użytej przez indeks. Konflikt nazwy trzeba rozstrzygnąć świadomie; automatyczne przemianowanie bez uzgodnienia może złamać kontrakt odbiorcy.
Nie traktuję kolejności wierszy jako zamiennika identyfikatora. Odbiorca może sortować, filtrować albo łączyć dane, więc „drugi rekord” nie jest trwałym opisem obiektu. Jeśli planujesz porównać dwa eksporty, stabilny klucz ułatwia wskazanie dodanych, usuniętych i zmienionych rekordów. Sam indeks pozycyjny po ponownym odczycie zwykle nie daje takiej gwarancji.
Braki zapisują się jako null
JSON ma wartość null, ale nie ma pandasowego pd.NA ani standardowej wartości JSON o nazwie NaN. to_json zapisuje brakujące wartości jako null. Nie istnieje parametr null_value, który zmieniałby ten zapis. Starszy przykład z takim argumentem kończył się błędem wywołania, dlatego zastępuję go pełnym przykładem z jawnym typem nullable.
# example: missing
import json
import pandas as pd
df = pd.DataFrame({"units": pd.Series([0, pd.NA], dtype="Int64")})
text = df.to_json(orient="records")
print(text)
print(json.loads(text)[1]["units"] is None)
Wynik to [{"units":0},{"units":null}] i True. Po parsowaniu przez standardowy moduł json wartość null staje się Pythonowym None. Zero pozostaje zerem. To rozróżnienie ma znaczenie przy sumach, średnich i ocenie kompletności danych. Brak pomiaru nie staje się pomiarem zerowym tylko dlatego, że odbiorca woli kolumnę bez pustych komórek.
Jeśli umowa wymaga napisu oznaczającego brak, przygotuj go osobno i opisz zmianę typu. Napis "null" jest tekstem, a null brakiem wartości JSON. Te reprezentacje nie są równoważne. Nie zastępuj też wszystkich braków tym samym znacznikiem bez sprawdzenia, czy źródło rozróżnia brak pomiaru, brak zastosowania i błąd pobrania. Tabela mogła już wcześniej utracić te różnice.
Force_ascii dotyczy zapisu znaków
Domyślnie force_ascii=True zapisuje znaki spoza ASCII za pomocą sekwencji ucieczki. force_ascii=False pozostawia je w czytelnej postaci. Obydwa teksty powinny po parsowaniu dać tę samą nazwę miasta. Parametr nie tłumaczy tekstu, nie usuwa polskich liter i nie wybiera kodowania dowolnego pliku wyjściowego za odbiorcę.
# example: unicode
import json
import pandas as pd
df = pd.DataFrame({"city": ["Łódź"]})
escaped = df.to_json(orient="records", force_ascii=True)
readable = df.to_json(orient="records", force_ascii=False)
print(readable)
print("\\u" in escaped, json.loads(escaped) == json.loads(readable))
Czytelny tekst to [{"city":"Łódź"}]; obydwa porównania zwracają True. Sprawdzam obecność sekwencji Unicode i zgodność sparsowanych danych, zamiast kopiować niepewny zapis znaków. Dla pliku, który ludzie będą oglądać w edytorze, force_ascii=False może ułatwić kontrolę. Nadal trzeba prawidłowo zapisać i odczytać bajty, na przykład jako UTF-8.
Wcięcia sterowane przez indent zmieniają czytelność tekstu, a nie jego logiczną zawartość. Rozbudowane wcięcia zwiększają rozmiar pliku. Dla małej próbki pomagają przejrzeć strukturę; dla dużego eksportu mogą być zbędne. Porównanie obiektów po parsowaniu pozwala odróżnić zmianę formatowania od faktycznej zmiany danych i uniknąć fałszywego alarmu przy różnicy białych znaków.
Daty zapisuję jawnie w ISO 8601
JSON nie ma osobnego typu daty. Data jest zapisywana jako tekst albo liczba zgodnie z przyjętą umową. W pandas format zależy od parametrów i orientacji; zapis epoch jest przestarzały od pandas 3.0. W nowych przykładach ustawiam date_format="iso" i jawnie wybieram precyzję. Odbiorca nadal musi wiedzieć, że pole oznacza chwilę w czasie.
# example: dates
import json
import pandas as pd
df = pd.DataFrame({"observed_at": pd.to_datetime(["2026-10-24T10:00:00+02:00"], utc=True)})
text = df.to_json(orient="records", date_format="iso", date_unit="ms")
value = json.loads(text)[0]["observed_at"]
print(value)
print(pd.Timestamp(value) == df.loc[0, "observed_at"])
Wartość to 2026-10-24T08:00:00.000Z, a porównanie daje True. Przed eksportem sprowadziłem datę do UTC. Zapis zachowuje chwilę, lecz nie przechowuje pierwotnego sposobu przedstawienia jej jako czasu z przesunięciem +02:00. Jeśli potrzebujesz także nazwy lokalnej strefy, przechowuj ją w osobnym uzgodnionym polu.
date_unit="ms" określa tutaj precyzję zapisu. Dane o dokładności mikrosekund lub nanosekund mogą wymagać dokładniejszej jednostki i testu odbiorcy. Jeżeli źródło zawiera daty bez strefy, najpierw ustal ich znaczenie. Samo dopisanie UTC do lokalnego czasu nie jest konwersją. W próbkach kontrolnych warto uwzględnić również brak daty i datę blisko granicy dnia.

Zapis do pliku sprawdzam przez jego odczyt
Po podaniu ścieżki metoda zapisuje dane i zwraca None. To inny kontrakt niż wywołanie bez ścieżki, które zwraca tekst. Nie używaj wartości zwróconej jako zawartości pliku. Poniższa próbka tworzy plik tymczasowy, odczytuje go standardowym parserem i sprawdza dane, które naprawdę znalazły się na dysku.
# example: file
import json
from pathlib import Path
from tempfile import TemporaryDirectory
import pandas as pd
with TemporaryDirectory(prefix="json-writing-") as folder:
path = Path(folder) / "records.json"
df = pd.DataFrame({"city": ["Łódź"], "units": [0]})
result = df.to_json(path, orient="records", force_ascii=False)
saved = json.loads(path.read_text(encoding="utf-8"))
print(result is None, saved == [{"city": "Łódź", "units": 0}])
Wynik to True True. W rzeczywistym procesie sprawdź również docelową lokalizację, dostępne miejsce i zachowanie przy ponownym wykonaniu. Domyślny zapis nie oznacza automatycznej historii wersji. Jeśli inny proces czyta plik w tym samym czasie, rozważ przygotowanie kompletnego pliku tymczasowego i jego kontrolowaną podmianę zgodnie z możliwościami używanego systemu plików.
Nie zapisuję pełnej tabeli roboczej tylko dlatego, że wszystkie kolumny są pod ręką. Wybieram pola przewidziane w umowie. Tabela analityczna może zawierać pomocnicze identyfikatory, notatki albo kolumny kontrolne niepotrzebne odbiorcy. Jawny wybór kolumn pomaga także wykryć przypadkowe rozszerzenie eksportu po dodaniu nowego pola w poprzednim etapie procesu.
JSON Lines zapisuje rekordy osobno
lines=True współpracuje z orient="records". Każdy rekord staje się osobnym dokumentem w jednej linii. To nie jest zwykła tablica JSON sformatowana z wcięciami. Odbiorca musi użyć parsera rozumiejącego taki układ albo czytać linie oddzielnie. Format jest wygodny przy przetwarzaniu partiami i dopisywaniu kolejnych rekordów.
# example: lines
import json
from pathlib import Path
from tempfile import TemporaryDirectory
import pandas as pd
with TemporaryDirectory(prefix="json-lines-") as folder:
path = Path(folder) / "records.jsonl"
pd.DataFrame({"id": ["A", "B"]}).to_json(path, orient="records", lines=True)
pd.DataFrame({"id": ["C"]}).to_json(path, orient="records", lines=True, mode="a")
records = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines()]
print([row["id"] for row in records], len(records))
Wynik ['A', 'B', 'C'] 3 pokazuje zapis i dopisanie trzeciego rekordu. Tryb mode="a" w tym interfejsie jest obsługiwany dla lines=True oraz orient="records". Nie dopisuj dwóch niezależnych tablic JSON do jednego pliku, zakładając, że powstanie poprawna wspólna tablica. Zwykłe sklejenie tekstów nie wykonuje takiej transformacji.
Dopisanie nie zapewnia unikalności rekordów. Powtórne wykonanie tej samej partii może dopisać ją ponownie, więc proces potrzebuje własnej reguły identyfikacji i potwierdzania przetworzonych danych. JSON Lines ułatwia podział strumienia na rekordy, lecz nie zapewnia transakcji, blokad ani odporności na każdą awarię zapisu. Dobierz sposób publikowania pliku do sposobu jego konsumpcji.
Precyzja liczby i typ tabeli to osobne sprawy
double_precision steruje liczbą miejsc dziesiętnych podczas zapisu liczb zmiennoprzecinkowych; maksymalna dopuszczalna wartość wynosi piętnaście. Nie odzyska precyzji utraconej wcześniej i nie zamieni przybliżonego typu na dokładny model dziesiętny. Przed eksportem określ, jaką dokładność obsługuje odbiorca i jaki błąd porównania jest dopuszczalny dla konkretnej miary.
Podobnie orient="table" dodaje schemat, ale nie jest obietnicą zachowania każdego możliwego obiektu Pythona w dowolnej bibliotece. Opis build_table_schema wyjaśnia metadane schematu. Pole pandas_version w tym schemacie opisuje wersję schematu, a niekoniecznie zainstalowaną wersję pandas. Nie używaj go jako jedynego dowodu środowiska wykonania.
Dla nieobsługiwanego obiektu można rozważyć default_handler, ale jego wynik staje się częścią umowy wymiany. Automatyczne str dla wszystkiego może ukryć problem i utracić strukturę. Jeżeli odbiorca potrzebuje dokładnego typu, wybierz format i schemat odpowiednie do tego wymagania. JSON jest formatem wymiany danych, a nie uniwersalną kopią wszystkich właściwości tabeli w pamięci.
Kontroluję wynik po ponownym odczycie
Ostatnia próbka łączy zapis i odczyt z jawnymi typami. records nie zawiera pełnego schematu pandas, więc importer dostaje mapę dtype. Dzięki temu tekstowe identyfikatory zachowują zera wiodące, a liczby całkowite dopuszczają braki. Porównanie obejmuje również typy i indeks odtworzony dla tej prostej tabeli.
# example: roundtrip
from io import StringIO
import pandas as pd
source = pd.DataFrame({"id": pd.Series(["001", "002"], dtype="string"), "units": pd.Series([0, pd.NA], dtype="Int64")})
text = source.to_json(orient="records")
restored = pd.read_json(StringIO(text), orient="records", dtype={"id": "string", "units": "Int64"}, convert_dates=False)
pd.testing.assert_frame_equal(source, restored)
print(restored["id"].tolist(), restored["units"].tolist())
Wynik to ['001', '002'] [0, <NA>]. Kontrola nie twierdzi, że każda tabela przejdzie identyczny cykl bez konfiguracji. Pokazuje konkretną umowę dla dwóch kolumn. Rozszerzając próbkę, dodaj daty, indeks biznesowy i pola opcjonalne oraz określ, co ma być zachowane dokładnie, a co może zostać świadomie znormalizowane.
Umowę sprawdzam na kilku poziomach
Przygotowanie kontroli zaczynam od rozdzielenia trzech pytań. Czy plik da się odczytać jako deklarowany format? Czy zawiera właściwy zestaw rekordów i pól? Czy odczytane wartości mają poprawne znaczenie? Parser odpowiada głównie na pierwsze pytanie. Dwa następne wymagają oczekiwanej liczby rekordów, znanych identyfikatorów oraz reguł dotyczących typów, braków i jednostek. Dzięki temu wiadomo, czego konkretnie dowodzi każdy wynik testu.
Sprawdź też różnicę między polem pominiętym a polem obecnym z null. Tabela ma wspólny zestaw kolumn, więc eksport rekordów zwykle zapisuje również pola z brakami. Jeżeli API wymaga pomijania nieobecnych pól, potrzebujesz dodatkowego przekształcenia struktury i osobnej reguły rozstrzygającej, które braki oznaczają pominięcie. Usuwanie wszystkich pustych pól może zmienić znaczenie operacji aktualizacji po stronie odbiorcy.
W teście współpracy warto użyć parsera, którym rzeczywiście posługuje się odbiorca. Odczyt tym samym pandas potwierdza naszą lokalną umowę, ale nie dowodzi zgodności każdej implementacji JSON. Szczególnie sprawdź obsługę dużych identyfikatorów liczbowych, precyzję dat i rozróżnienie liczby od napisu. Jeśli identyfikator nie służy do obliczeń, zapis tekstowy może być częścią kontraktu, dzięki której jego znaki zostaną zachowane bez interpretacji liczbowej.
Dodanie kolumny do eksportu również jest zmianą kontraktu. Część odbiorców zignoruje nieznane pole, a część je odrzuci. Dlatego testuję jawny zestaw pól zamiast zakładać, że więcej informacji zawsze będzie pomocne. Podobnie przemianowanie kolumny nie jest kosmetyczną korektą, kiedy odbiorca odwołuje się do niej po nazwie. Dla procesu powtarzanego przez kilka zespołów potrzebna jest uzgodniona procedura wprowadzania takich zmian.
Przy porównywaniu dwóch eksportów ustal znaczenie kolejności. Lista rekordów zachowuje porządek, lecz w niektórych zadaniach ważny jest zbiór identyfikatorów i wartości, niezależnie od sortowania. W innych kolejność reprezentuje przebieg zdarzeń. Test powinien odpowiadać tej umowie. Nie sortuj automatycznie wszystkiego przed porównaniem, jeśli mogłoby to ukryć błędną kolejność danych, i nie zgłaszaj awarii wyłącznie z powodu dozwolonego przestawienia rekordów.
Zachowaj małą próbkę oczekiwanego dokumentu razem z opisem pól. Taki przykład pomaga osobie tworzącej importer szybciej zauważyć, czy indeks jest polem, czy metadanymi, oraz czy daty mają strefę. Dane próbki powinny być syntetyczne i obejmować kłopotliwe przypadki, zamiast kopiować rzeczywiste rekordy. Wersję biblioteki i wyniki kontroli zapisuj osobno od danych przeznaczonych do przetwarzania przez odbiorcę.
Ćwiczenie: sprawdź umowę z odbiorcą
Zmień indeks na identyfikatory tekstowe, dodaj brak daty i kolumnę z nazwą miasta. Najpierw zapisz oczekiwane rekordy, potem wykonaj eksport. Sprawdź, które informacje zachował records, a które musisz przenieść do kolumn. Porównaj także wynik odczytu z jawnymi typami oraz bez nich. Zanotuj różnice, zamiast uznawać brak wyjątku za pełne potwierdzenie zgodności.
Na końcu wykonaj pusty eksport i eksport jednego rekordu. Ustal, czy odbiorca akceptuje pustą listę albo pusty plik JSON Lines. Dla pełniejszego procesu połącz read_json, jedną regułę replace i kontrolowany zapis. Liczba wierszy, identyfikatory oraz liczba braków powinny mieć uzasadniony wynik na każdym etapie.
Najważniejsze w pigułce
Czy pandas ma funkcję pd.to_json?
Eksport wywołujesz jako metodę obiektu, na przykład df.to_json(). Bez ścieżki otrzymasz tekst; po podaniu pliku metoda zapisuje wynik i zwraca None.
Jak ustawić null_value w to_json?
Nie ma takiego parametru. Braki są serializowane jako JSON null. Jeżeli kontrakt wymaga innego znacznika, przygotuj go jawnie przed eksportem i sprawdź zmianę typów oraz znaczenia danych.
Czy orient='records' zachowuje indeks?
Nie zapisuje etykiet indeksu. Jeśli indeks zawiera identyfikator rekordu, przenieś go do nazwanej kolumny lub wybierz uzgodnioną orientację zachowującą indeks.
Przy pracy nad artykułem użyto w zautomatyzowany sposób sztucznej inteligencji. Jeśli też chcesz tak umieć, sprawdź ten projekt.
