Jak wczytać plik płaski w Python z pomocą biblioteki Pandas?

Czytanie danych – jedna z podstawowych czynności w data science. Każdy, kto na co dzień pracuje z danymi, wykona ją w swoim życiu zawodowym setki (o ile nie tysiące) razy. Warto zatem wiedzieć jak robić to w sposób prawidłowy.

Z tego artykułu dowiesz się: 1. Jak wczytać dane z pliku płaskiego (csv/txt) w Pandas? 2. Jak uniknąć najpowszechniejszych błędów i na co zwrócić szczególną uwagę? 3. Jak kilkukrotnie zmniejszych ilość pamięci, jaką zajmuje plik po wczytaniu?

TXT, CSV i plik płaski - kilka definicji na początek

Plik CSV - (ang. comma-separated values) jest formatem przechowywania danych w plikach tekstowych. Domyślnie (jak sama nazwa wskazuje) ma wartości rozdzielone przecinkiem.

Plik TXT - plik posiadający zazwyczaj rozszerzenie .txt. Jest plikiem tekstowym, przechowującym dane w postaci alfanumerycznej. Może (choć oczywiście nie musi) przechowywać dane tabelaryczne. Jest więc pewnym uogólnieniem formatu CSV.

Plik płaski - jest to po prostu stara, alternatywna nazwa dla pliku tekstowego. 🙂

Najczęstsze niespójności pomiędzy różnymi plikami płaskimi

Tym, co stanowi największą bolączkę podczas wczytywania plików płaskich, są niespójności w ich strukturze. Poniżej wymieniam najpopularniejsze z nich:

  • Używanie różnych znaków jako separatora wartości - dosyć powszechne są średniki i tabulatory. Niekiedy można się spotkać ze znakiem "|".
  • Używanie różnych separatorów dziesiętnych - w zależności od regionu może być to kropka lub przecinek.
  • Używanie różnych zapisów daty - 12/12/2012, 2012-12-12, 12.12.2012, etc.
  • Użyty separator dziesiętny przy wartościach całkowitoliczbowych - jeśli w danej kolumnie występują same liczby całkowite, to nie ma potrzeby, by wczytywać ją w formie 123.000.
  • Data i godzina w osobnych kolumnach - to może, choć nie musi być problemem. W Pandas do przechowywania obu zazwyczaj używamy formatu "datetime", który przechowuje zarówno datę, jak i godzinę.
  • Obecność/brak nagłówka/stopki - czasem np. statystyki podsumowujące zostają zapisane w ostatnim wierszu pliku.

Wiemy już, z jakiego typu problemami będziemy się mierzyć. Przejdźmy do części praktycznej i wczytajmy przykładowy zbiór.

Czytanie zbioru z pliku płaskiego - podejście #1

Importuję bibliotekę Pandas.

In [1]:
import pandas as pd
import warnings
warnings.filterwarnings('ignore')

Użyję podstawowej metody dostępnej w Pandas - pd.read_csv.

In [2]:
df = pd.read_csv('data/household_power_consumption.txt')

Sprawdźmy, czy wszystko gra...

In [3]:
df.head()
Out[3]:
Date;Time;Global_active_power;Global_reactive_power;Voltage;Global_intensity;Sub_metering_1;Sub_metering_2;Sub_metering_3
0 16/12/2006;17:24:00;4.216;0.418;234.840;18.400...
1 16/12/2006;17:25:00;5.360;0.436;233.630;23.000...
2 16/12/2006;17:26:00;5.374;0.498;233.290;23.000...
3 16/12/2006;17:27:00;5.388;0.502;233.740;23.000...
4 16/12/2006;17:28:00;3.666;0.528;235.680;15.800...

Widać pierwszy problem: separator kolumn użyty w zbiorze, to średnik. W Pandas separatorem domyślnie jest przecinek.

Czytanie zbioru z pliku płaskiego - podejście #2

Ustawiam parametr sep=';'.

In [4]:
df = pd.read_csv('data/household_power_consumption.txt', sep=';')
In [5]:
df.head()
Out[5]:
Date Time Global_active_power Global_reactive_power Voltage Global_intensity Sub_metering_1 Sub_metering_2 Sub_metering_3
0 16/12/2006 17:24:00 4.216 0.418 234.840 18.400 0.000 1.000 17.0
1 16/12/2006 17:25:00 5.360 0.436 233.630 23.000 0.000 1.000 16.0
2 16/12/2006 17:26:00 5.374 0.498 233.290 23.000 0.000 2.000 17.0
3 16/12/2006 17:27:00 5.388 0.502 233.740 23.000 0.000 1.000 17.0
4 16/12/2006 17:28:00 3.666 0.528 235.680 15.800 0.000 1.000 17.0

Wygląda nieporównywalnie lepiej, ale mamy ostrzeżenie na czerwono. Dotyczy typów zmiennych. Sprawdźmy je.

In [6]:
df.dtypes
Out[6]:
Date                      object
Time                      object
Global_active_power       object
Global_reactive_power     object
Voltage                   object
Global_intensity          object
Sub_metering_1            object
Sub_metering_2            object
Sub_metering_3           float64
dtype: object

Wszystko zmienne, z wyjątkiem ostatniej zostały wczytane jako "object". Dla zmiennych numerycznych nie jest to optymalny format. 😉

Próbowałem wymusić konwersję zmiennej "Global_active_power" do typu float. Nie udało się. Otrzymałem błąd:

ValueError: could not convert string to float: '?'

Brakujące bądź błędne wartości są prawdopodobnie oznaczone znakiem zapytania. Uwzględnijmy tę informację podczas czytania zbioru. 😉

By pokazać, jaki dodatkowy problem stwarza niewłaściwe wczytanie pliku, sprawdzę jego rozmiar po wczytaniu.

In [7]:
df.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2075259 entries, 0 to 2075258
Data columns (total 9 columns):
 #   Column                 Dtype  
---  ------                 -----  
 0   Date                   object 
 1   Time                   object 
 2   Global_active_power    object 
 3   Global_reactive_power  object 
 4   Voltage                object 
 5   Global_intensity       object 
 6   Sub_metering_1         object 
 7   Sub_metering_2         object 
 8   Sub_metering_3         float64
dtypes: float64(1), object(8)
memory usage: 1007.3 MB

1007.3 MB - zanotujmy to jako punkt wyjściowy. 😉

Czytanie zbioru z pliku płaskiego - podejście #3

Ustawiam parametr na_values='?'.

In [8]:
df = pd.read_csv('data/household_power_consumption.txt', sep=';', na_values='?')
In [9]:
df.dtypes
Out[9]:
Date                      object
Time                      object
Global_active_power      float64
Global_reactive_power    float64
Voltage                  float64
Global_intensity         float64
Sub_metering_1           float64
Sub_metering_2           float64
Sub_metering_3           float64
dtype: object

Teraz wygląda to znacznie lepiej. 🙂

In [10]:
df.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2075259 entries, 0 to 2075258
Data columns (total 9 columns):
 #   Column                 Dtype  
---  ------                 -----  
 0   Date                   object 
 1   Time                   object 
 2   Global_active_power    float64
 3   Global_reactive_power  float64
 4   Voltage                float64
 5   Global_intensity       float64
 6   Sub_metering_1         float64
 7   Sub_metering_2         float64
 8   Sub_metering_3         float64
dtypes: float64(7), object(2)
memory usage: 370.0 MB

Zużycie pamięci spadło z 1007.3 MB na 370. I to przez jeden dodatkowy parametr. Nieźle! 🙂 Widać jednak, że data i czas są wczytywane błędnie. Jest to problem prowadzący do potencjalnych kłopotów (np. podczas sortowania, dzielenia zbioru, walidacji krzyżowej, etc.).

Czytanie zbioru z pliku płaskiego - podejście #4

Podczas wczytywania pliku płaskiego, parsuję datę i godzinę dostępne w zbiorze. Używam parametrów: date_parser i parse_dates.

In [11]:
dateparser = lambda x: pd.to_datetime(x, format = '%d/%m/%Y %H:%M:%S')
In [12]:
df = pd.read_csv('data/household_power_consumption.txt',
                 sep=';', 
                 na_values='?', 
                 date_parser=dateparser, 
                 parse_dates={'Date_time':['Date', 'Time']})
In [13]:
df.dtypes
Out[13]:
Date_time                datetime64[ns]
Global_active_power             float64
Global_reactive_power           float64
Voltage                         float64
Global_intensity                float64
Sub_metering_1                  float64
Sub_metering_2                  float64
Sub_metering_3                  float64
dtype: object
In [14]:
df.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2075259 entries, 0 to 2075258
Data columns (total 8 columns):
 #   Column                 Dtype         
---  ------                 -----         
 0   Date_time              datetime64[ns]
 1   Global_active_power    float64       
 2   Global_reactive_power  float64       
 3   Voltage                float64       
 4   Global_intensity       float64       
 5   Sub_metering_1         float64       
 6   Sub_metering_2         float64       
 7   Sub_metering_3         float64       
dtypes: datetime64[ns](1), float64(7)
memory usage: 126.7 MB

To, co zwraca uwagę, to użycie we wszystkich zmiennych numerycznych typu float64. Nie jest to optymalne. Rzućmy okiem na wartości, jakie przyjmują poszczególne zmienne.

In [15]:
df.agg(['min', 'median', 'max']).round(3)
Out[15]:
Date_time Global_active_power Global_reactive_power Voltage Global_intensity Sub_metering_1 Sub_metering_2 Sub_metering_3
min 2006-12-16 17:24:00 0.076 0.00 223.20 0.2 0.0 0.0 0.0
median 2008-12-06 07:13:00 0.602 0.10 241.01 2.6 0.0 0.0 1.0
max 2010-11-26 21:02:00 11.122 1.39 254.15 48.4 88.0 80.0 31.0

Są relatywnie niewielkie. +/- 0-254. Można je spokojnie zamienić na float16.

Czytanie zbioru z pliku płaskiego - podejście #5

In [16]:
dateparser = lambda x: pd.to_datetime(x, format = '%d/%m/%Y %H:%M:%S')
In [18]:
import numpy as np
df = pd.read_csv('data/household_power_consumption.txt',
                 sep=';', 
                 na_values='?',
                 dtype={
                     'Global_active_power':np.float16, 
                     'Global_reactive_power':np.float16, 
                     'Voltage':np.float16, 
                     'Global_intensity':np.float16, 
                     'Sub_metering_1':np.float16, 
                     'Sub_metering_2':np.float16, 
                     'Sub_metering_3':np.float16
                 },
                 date_parser=dateparser, 
                 parse_dates={'Date_time':['Date', 'Time']})
In [19]:
df.info(memory_usage='deep')
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2075259 entries, 0 to 2075258
Data columns (total 8 columns):
 #   Column                 Dtype         
---  ------                 -----         
 0   Date_time              datetime64[ns]
 1   Global_active_power    float16       
 2   Global_reactive_power  float16       
 3   Voltage                float16       
 4   Global_intensity       float16       
 5   Sub_metering_1         float16       
 6   Sub_metering_2         float16       
 7   Sub_metering_3         float16       
dtypes: datetime64[ns](1), float16(7)
memory usage: 43.5 MB

Zbiór teraz zajmuje w pamięci jedynie 43.5 MB. Oznacza to, że poprzez wykonanie powyższych operacji udało się obniżyć użycie pamięci o ponad 95%! 🙂

Podsumowanie

Jeśli masz jakieś pytania, to proszę, podziel się nimi w komentarzu pod wpisem - zapraszam do dyskusji. Jeśli artykuł przypadł Ci do gustu, to proszę, podziel się nim w mediach społecznościowych ze swoimi znajomymi. Będę bardzo wdzięczny. 🙂

PODOBAŁ CI SIĘ TEN ARTYKUŁ?

Jeśli tak, to zarejestruj się, by otrzymywać informacje o nowych wpisach.
Dodatkowo w prezencie wyślę Ci bezpłatny poradnik :-)

2 Komentarze

  1. Kolumny dot. sub_metering na pierwszy rzut oka wyglądają na integery. Czy pandas ogarnąłby, że tam są integery, a daje float bo gdzieś jakaś komórka miała flot’a

    • Cześć Bart! Dzięki za komentarz i brawo za spostrzegawczość. 🙂

      Tak, to są integery. Pandas normalnie powinien to ogarnąć, ale w tym przypadku zmienne „Sub_metering” zawierają brakujące wartości, dlatego muszą być przechowywane jako float. Jeżeli będziemy chcieli wymusić konwersję zmiennej do liczby całkowitej, to Pandas zwróci błąd:
      > „Cannot convert non-finite values (NA or inf) to integer”

Dodaj komentarz

Twój adres email nie zostanie opublikowany.


*