Pandas (I)

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))</code>
# Ejemplos de comportamiento como numpy array
print('Se comporta como un numpy array:')
print('================================')
print('&gt;&gt;&gt; serie.max()n{}'.format(serie.max()))
print('&gt;&gt;&gt; serie.sum()n{}'.format(serie.sum()))
print('&gt;&gt;&gt; serie.abs()n{}'.format(serie.abs()))
print('&gt;&gt;&gt; serie[serie &gt; 0]n{}'.format(serie[serie &gt; 0]))
#...
print('n')
# Ejemplos de comportamiento como diccionario
print("Se comporta como un diccionario:")
print("================================")
print("&gt;&gt;&gt; serie['a']n{}".format(serie['a']))
print("&gt;&gt;&gt; 'a' in serien{}".format('a' in serie))
print("&gt;&gt;&gt; '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!!

Kiko Correoso

Licenciado y PhD en Ciencias Físicas, especializado en temas de física, meteorología, climatología, energías renovables, estadística, aprendizaje automático, análisis y visualización de datos. Apasionado de Python y su comunidad. Fundador de pybonacci y editor del sitio en el que se divulga Python, Ciencia y el conocimiento libre en español.

More Posts

Follow Me:
TwitterLinkedIn

5 thoughts on “Pandas (I)

  1. Kiko, gracias por el curso.

    Estoy comenzando con Python. IPython Notebook me corre con Python 2. ¿Cómo puedo hacer que corra con Python 3?

    1. instala Python3 y luego ipython para Python3.

      Puedes usar un virtualenv o un entorno de conda.

      Ahora estoy con el móvil pero luego te dejo un par de enlaces

  2. Tengo una duda:

    Cuando construyes el DataFrame a partir de un numpy array 2D, no arrojarría un dataframe de 5×3 vacío? Cuándo le has pasado el random…?

    Muchas Gracias.
    como diría mi hija:
    “Maestro”

  3. No.

    El DataFrame se te creará con lo que le digas que lo cree. Si usas numpy.random.randn(5,3), por ejemplo, te creará un dataframe con valores aleatorios con *shape* (5, 3).

    Si quieres crear un dataframe vacio puedes hacer ‘df = pd.DataFrame()’ pero este dataframe no tendrá estructura (filas x columnas) hasta que le metas datos.

Leave a Reply

Your email address will not be published. Required fields are marked *