Pandas (I)

Por Kiko Correoso

Antes de nada, el contexto, para esta serie de entradas se va a usar lo siguiente:

Versión de Python:      3.3.1 (default, Apr 10 2013, 19:05:32) 
[GCC 4.6.3]
Versión de Pandas:      0.13.1
Versión de Numpy:       1.8.1
Versión de Matplotlib:  1.3.1

Y sin más preámbulos...

¿Qué es Pandas?

Pandas es una librería que proporciona estructuras de datos flexibles y permite trabajar con la información de forma eficiente (gran parte de Pandas está implementado usando C/Cython para obtener un buen rendimiento).

Funciona muy bien cuando nos toca trabajar con:

  • Datos heterogéneos que pueden distribuirse de forma tabular.
  • Series temporales
  • Matrices
  • ...

La página oficial se encuentra en el siguiente enlace.

Estructuras de datos

Pandas ofrece varias estructuras de datos que nos resultarán de mucha utilidad y que vamos a ir viendo poco a poco. Todas las posibles estructuras de datos que ofrece a día de hoy son:

  • Series (y TimeSeries)
  • DataFrame
  • Panel
  • Panel4D
  • PanelND

Series

En una instancia de la clase Series podremos almacenar arrays o vectores con índice o etiqueta. Si no usamos índice o etiqueta nos lo numerará con un índice de forma interna. La forma básica de crear una Series sería:

>>> s = Series(data, index=index)

donde data es el vector de datos e index (opcional) es el vector de índices que usará la serie. Si los índices son datos de fechas directamente se creará una instancia de una TimeSeries en lugar de una instacia de Series.

Veamos un ejemplo de como crear este tipo de contenedor de datos. Primero vamos a crear una serie y pandas nos creará índices automáticamente, segundo vamos a crear una serie donde nosotros le vamos a decir los índices que queremos usar y, tercero, vamos a crear una serie temporal usando índices que son fechas.

# serie con índices automáticos
serie = pd.Series(np.random.randn(10))
print(u'Serie con índices automáticos n{} n'.format(serie))
print(type(serie))

Cuyo resultado sería:

Serie con índices automáticos
0 -1.731792
1 0.492089
2 0.190005
3 0.498570
4 -0.129411
5 0.496611
6 -0.561550
7 -0.981822
8 -0.471803
9 -0.255938
dtype: float64

<class 'pandas.core.series.Series'>
# serie con índices definidos por mi
serie = pd.Series(np.random.randn(4),
                  index = ['itzi','kikolas','dieguete','nicolasete'])
print(u'Serie con índices definidos n{} n'.format(serie))
print(type(serie))

Cuyo resultado sería el siguiente:

Serie con índices definidos
itzi       0.130432
kikolas   -2.378303
dieguete   0.951302
nicolasete 1.846942
dtype: float64

<class 'pandas.core.series.Series'>
# serie(serie temporal) con índices que son fechas
serie = pd.Series(np.random.randn(31),
                  index = pd.date_range('2013/01/01', periods = 31))
print(u'Serie temporal con índices de fechas n{} n'.format(serie))
print(type(serie))

Cuyo resultado sería:

Serie temporal con índices de fechas
2013-01-01  0.086782
2013-01-02 -0.274399
2013-01-03 -0.919958
2013-01-04  0.749879
2013-01-05 -1.739752
2013-01-06  0.861299
2013-01-07 -0.797413
2013-01-08 -0.650584
2013-01-09  0.880755
2013-01-10 -0.235406
2013-01-11  0.134650
2013-01-12 -0.255786
2013-01-13 -0.068295
2013-01-14  0.010727
2013-01-15  0.259768
2013-01-16  2.449214
2013-01-17 -1.452189
2013-01-18 -0.846709
2013-01-19 -0.835064
2013-01-20 -0.073190
2013-01-21  0.853177
2013-01-22 -0.239683
2013-01-23  0.149482
2013-01-24 -0.233044
2013-01-25  0.073302
2013-01-26 -1.769805
2013-01-27 -1.016134
2013-01-28  0.378350
2013-01-29  0.190374
2013-01-30 -1.566578
2013-01-31  1.601782
Freq: D, dtype: float64 

<class 'pandas.core.series.Series'>

En los ejemplos anteriores hemos creado las series a partir de un numpy array pero las podemos crear a partir de muchas otras cosas: listas, diccionarios, numpy arrays,... Veamos ejemplos:

serie_lista = pd.Series([i*i for i in range(10)])
print('Serie a partir de una lista n{} n'.format(serie_lista))
dicc = {'cuadrado de {}'.format(i) : i*i for i in range(10)}
serie_dicc = pd.Series(dicc)
print('Serie a partir de un diccionario n{} n'.format(serie_dicc))
serie_serie = pd.Series(serie_dicc.values)
print('Serie a partir de los valores de otra (pandas)serie n{} n'.format(serie_serie))
serie_cte = pd.Series(-999, index = np.arange(10))
print('Serie a partir de un valor constante n{} n'.format(serie_cte))
#...

Y el resultado del código anterior sería:

Serie a partir de una lista
0 0
1 1
2 4
3 9
4 16
5 25
6 36
7 49
8 64
9 81
dtype: int64

Serie a partir de un diccionario

cuadrado de 0 0

cuadrado de 1 1

cuadrado de 2 4

cuadrado de 3 9

cuadrado de 4 16

cuadrado de 5 25

cuadrado de 6 36

cuadrado de 7 49

cuadrado de 8 64

cuadrado de 9 81

dtype: int64

Serie a partir de los valores de otra (pandas)serie

0 0

1 1

2 4

3 9

4 16

5 25

6 36

7 49

8 64

9 81

dtype: int64

Serie a partir de un valor constante

0 -999

1 -999

2 -999

3 -999

4 -999

5 -999

6 -999

7 -999

8 -999

9 -999

dtype: int64

Una serie (Series o TimeSeries) se puede manejar igual que si tuviéramos un numpy array de una dimensión o igual que si tuviéramos un diccionario. Vemos ejemplos de esto:

serie = pd.Series(np.random.randn(10),
                  index = ['a','b','c','d','e','f','g','h','i','j'])
print('Serie que vamos a usar en este ejemplo: n{}nn'.format(serie))
# Ejemplos de comportamiento como numpy array
print('Se comporta como un numpy array:')
print('================================')
print('>>> serie.max()n{}'.format(serie.max()))
print('>>> serie.sum()n{}'.format(serie.sum()))
print('>>> serie.abs()n{}'.format(serie.abs()))
print('>>> serie[serie > 0]n{}'.format(serie[serie > 0]))
#...
print('n')
# Ejemplos de comportamiento como diccionario
print("Se comporta como un diccionario:")
print("================================")
print(">>> serie['a']n{}".format(serie['a']))
print(">>> 'a' in serien{}".format('a' in serie))
print(">>> 'z' in serien{}".format('z' in serie))

Y el resultado será:

Serie que vamos a usar en este ejemplo:
a -0.663462
b 0.893211
c -0.312062
d -1.196054
e 1.608529
f -0.045494
g -0.192866
h 1.681370
i -0.503151
j -0.078362
dtype: float64

Se comporta como un numpy array:

================================

>>> serie.max()

1.6813703206498873

>>> serie.sum()

1.1916586064903802

>>> serie.abs()

a 0.663462

b 0.893211

c 0.312062

d 1.196054

e 1.608529

f 0.045494

g 0.192866

h 1.681370

i 0.503151

j 0.078362

dtype: float64

>>> serie[serie > 0]

b 0.893211

e 1.608529

h 1.681370

dtype: float64

Se comporta como un diccionario:

================================

>>> serie['a']

-0.663462354872233

>>> 'a' in serie

True

>>> 'z' in serie

False

Las operaciones están 'vectorizadas' y se hacen elemento a elemento con los elementos alineados en función del índice. Si se hace, por ejemplo, una suma de dos series, si en una de las dos series no existe un elemento, i.e. el índice no existe en la serie, el resultado para ese índice será nan. En resumen, estamos haciendo una unión de los índices y funciona diferente a los numpy arrays. Se puede ver el esquema en el siguiente ejemplo:

s1 = serie[1:]
s2 = serie[:-1]
suma = s1 + s2
print(' s1 s2 s1 + s2')
print('------------------ ------------------ ------------------')
for clave in sorted(set(list(s1.keys()) + list(s2.keys()))):
print('{0:1} {1:20} + {0:1} {2:20} = {0:1} {3:20}'.format(clave,
s1.get(clave),
s2.get(clave),
suma.get(clave)))
# En la anterior línea de código uso el método get para no obtener un KeyError
# como sí obtendría si uso, p.e., s1['a']

Cuyo resultado será:

s1 s2 s1 + s2
------------------ ------------------ ------------------
a  None                + a -0.663462354872233   = a  nan
b  0.8932105297090326  + b  0.8932105297090326  = b  1.7864210594180652
c -0.31206215624310224 + c -0.31206215624310224 = c -0.6241243124862045
d -1.1960537478238258  + d -1.1960537478238258  = d -2.3921074956476516
e  1.6085289802454341  + e  1.6085289802454341  = e  3.2170579604908682
f -0.0454943963901047  + f -0.0454943963901047  = f -0.0909887927802094
g -0.19286571253906507 + g -0.19286571253906507 = g -0.38573142507813013
h  1.6813703206498873  + h  1.6813703206498873  = h  3.3627406412997747
i -0.503150771440017   + i -0.503150771440017   = i -1.006301542880034
j -0.07836208480562608 + j  None                = j  nan

DataFrame

Un DataFrame se puede ver como si fuera una tabla con índices para las filas y las columnas. Es algo similar a lo que tenemos en una hoja de cálculo, una tabla de una BBDD SQL o un diccionario de series Pandas. Esta será la estructura de datos más habitual a usar. El DataFrame se puede crear a partir de muchas otras estructuras de datos:

  • A partir de un diccionario de numpy arrays de 1D, diccionarios de listas, diccionarios de diccionarios o diccionarios de Series
  • A partir de un numpy array de 2D
  • A partir de un numpy array estructurado (structured ndarray o record ndarray)
  • A partir de una Series de Pandas
  • A partir de otro DataFramede Pandas

Además de los datos, podemos definir los índices (las etiquetas para las filas) o las columnas (etiquetas para las mismas). Si no se define nada al crear el DataFrame se usarán normas del sentido común para nombrar los índices de filas y columnas.

Veamos un poco de código para ver esto:

df_lista = pd.DataFrame({'a': [11,12,13], 'b': [21,22,23]})
print('DataFrame a partir de un diccionario de listas n{} n'.format(df_lista))
df_np1D = pd.DataFrame({'a': np.arange(3)**2, 'b': np.random.randn(3)})
print('DataFrame a partir de un diccionario de 1D ndarrays n{} n'.format(df_np1D))
df_np2D = pd.DataFrame(np.empty((5,3)),
index = ['primero','segundo','tercero','cuarto','quinto'],
columns = ['velocidad', 'temperatura','presion'])
print('DataFrame a partir de un 2D ndarray n{} n'.format(df_np2D))
df_df = pd.DataFrame(df_np2D, index = ['primero','segundo','tercero'])
df_df.index = ['first','second','third']
print('DataFrame a partir de los valores de otro (pandas)DataFrame n{} n'.format(df_df))
#...

Y el resultado de todo lo anterior sería:

DataFrame a partir de un diccionario de listas
a b
0 11 21
1 12 22
2 13 23


[3 rows x 2 columns]

DataFrame a partir de un diccionario de 1D ndarrays

a    b

0  0  -0.818739

1  1  -1.473575

2  4  -0.014659

[3 rows x 2 columns]

DataFrame a partir de un 2D ndarray

velocidad          temperatura      presion

primero     1.397157e-306   4.311076e-314   0.000000e+00

segundo  1.670358e-179  -1.238962e-65   -2.110672e-44

tercero    -3.645480e-42     0.000000e+00  -1.561315e-65

cuarto     -2.623582e-42     0.000000e+00   1.519743e-314

quinto    -2.578083e-42     0.000000e+00    2.413050e-312

[5 rows x 3 columns]

DataFrame a partir de los valores de otro (pandas)DataFrame

velocidad        temperatura      presion

first         1.397157e-306  4.311076e-314    0.000000e+00

second  1.670358e-179 -1.238962e-65    -2.110672e-44

third      -3.645480e-42    0.000000e+00   -1.561315e-65

[3 rows x 3 columns]

Podemos construir un DataFrame a partir de constructores alternativos como pd.DataFrame.from_dict, pd.DataFrame.from_records o pd.DataFrame.from_items.

Panel, Panel4D, PanelND

En general, los paneles son para tipos de datos de más de dos dimensiones. No los vamos a cubrir ya que se consideran un pelín más complejos, de uso menos habitual y/o se encuentran en estado experimental con lo que pueden cambiar bastante en el corto/medio plazo. Se puede consultar la documentación oficial pulsando sobre:

  • Panel
  • Panel4D
  • PanelND

Y de momento es suficiente por hoy.  En breve dejaremos una segunda parte... ¡¡Estad atentos!!


photo

Kiko es doctor en ciencias físicas y esta especializado en física de la atmósfera, meteorología y climatología. Además de estar en las nubes es especialista en temas de energías renovables en Iberdrola. Ávido de seguir mejorando siempre está abierto a participar en nuevos proyectos y retos por lo que no dudes en contactarle si necesitas servicios especializados de Python o meteorología.

Comentarios