Pandas como interfaz SQL

Mini tutorial de SQL para científicos

Hoy en día no se puede seguir trabajando con ficheros de texto habiendo tantas alternativas y, sobretodo, a medida que el tamaño de la información crece y crece y se hace inmanejable tratar ficheros de varios cientos de Mb. Es por ello que hoy vamos a ver por encima cómo podemos hacer consultas, modificar y crear nueva información en una base de datos SQL.
[Descargo de responsabilidad] Esto no pretende ser un tutorial ni algo serio y riguroso, solo un análisis superficial sobre lo que puede ofrecer el manejo de SQL para trabajar con datos y va dirigido, principalmente, a científicos (o no) que se manejan todo el día con ficheros *.dat, *.txt o *.csv.

¿Qué es SQL?

SQL es un acrónimo para Structured Query Language. Es un lenguaje que nos permite acceder a bases de datos SQL (bases de datos relacionales). Un RDBMS (Relational DataBase Management System) es un sistema que nos permite acceder, crear, editar y gestionar bases de datos relacionales. Existen RDBMS muy populares como MySQL (MariaDB), PostgreSQL o SQLite. En el presente tutorial vamos a trabajar con SQLite por simplicidad y porque viene disponible con CPython.

¿Y Pandas?

Pandas dispone de funcionalidad que nos permite leer y escribir información en bases de datos relacionales.

Vamos a crear una base de datos SQLite

Las bases de datos SQLite son bases de datos que no necesitan un servidor y que se guardan en disco. Para más información pulsa aquí. Podéis inspeccionar la base de datos que vayamos a crear, bastante simple, con SQLite manager, un addon para firefox.

Como siempre, primero importamos todo lo necesario

import sqlite3
import datetime as dt

import pandas as pd
import numpy as np
Mi configuración es la siguiente:

%load_ext version_information
%version_information pandas, numpy
Software Version
Python 3.4.0 64bit [GCC 4.8.2]
IPython 3.0.0
OS Linux 3.13.0 24 generic x86_64 with LinuxMint 17 qiana
pandas 0.15.2
numpy 1.9.2
Thu Mar 12 20:07:15 2015 CET
Primero necesitamos poder conectar con la base de datos. Esto es de lo poco que diferirá con respecto a otros RDBMSs.

conn = sqlite3.connect('pybodb.sqlite')

# ejemplo con PostgreSQL usando psycopg2
# import psycopg2
# conn = psycopg2.connect(database='ejemplodb', user='kiko') 

# ejemplo con MS ACCESS usando pyodbc (sí, en el trabajo tengo que usar estas cosas)
# import pyodbc
# conn = pyodbc.connect("Driver={Microsoft Access Driver (*.mdb, *.accdb)};DBQ=ejemplodb;") 

# ...
Ahora que ya tenemos una conexión a la base de datos Pandas se puede encargar del trabajo sucio de 'hablar' con la base de datos y ayudarnos a interactuar directamente con los datos de la forma habitual y potente de Pandas.

Vamos a crear un DataFrame que usaremos como una tabla para insertar en la base de datos. Este DataFrame tendrá una columna de fechas, una de medidas de temperatura promedio diaria (inventada), una de precipitación acumulada en 24h (inventada), una columna con el tipo de sensor que midió la temperatura ese día y una última con el sensor que midió la precipitación.

# fechas para cada día del 2014
fechas = pd.date_range(dt.datetime(2014, 1, 1), dt.datetime(2014, 12, 31))
# Temperatura media diaria durante 2014 en algún lugar del hemisferio Norte
tmed = (np.random.randint(-5, 5, size = 365) + 
        20 * np.cos(np.arange(0 - 180, 365 - 180) * 2 * np.pi / 365) 
        + 10)
# Precipitación acumulada en 24h
prec = (20 * np.abs(np.random.randn(365) * 
        np.cos(np.arange(0, 365) * 2 * np.pi / 365)))
# Sensor que midió la temperatura
marcaT = np.random.choice(['marcaT1', 'marcaT2', 'marcaT3'], size = 365)
# Sensor midió la precipitación
marcaP = np.random.choice(['marcaP1', 'marcaP2'], size = 365)

# Creamos el dataframe y lo guardamos en una tabla llamada 'datos'
df = pd.DataFrame(
        np.array([fechas.values, tmed, prec, marcaT, marcaP]).T,
        columns = ['fecha', 'tmedia','precipitacion','sensorT','sensorP'])
df['fecha'] = pd.to_datetime(df['fecha'])
df.to_sql('datos', con = conn, dtype = {'time': 'TIMESTAMP'})
Vamos a crear una segunda tabla para añadir un poco de complejidad a las consultas que hagamos posteriormente a la base de datos. Esta tabla contendrá información de los sensores usados para las medidas.

# fechas para cada día del 2014
sensores = ['marcaT1', 'marcaT2', 'marcaT3',
            'marcaP1', 'marcaP2']

# Precisión de los sensores
precision = [0.1, 0.5, 1,
             2, 5]

df = pd.DataFrame({'sensores': sensores, 'precision': precision})
df.to_sql('sensores', con = conn)

Consultando la base de datos

Dentro de SQL tenemos comandos que se pueden integrar en diferentes categorías. Ahora vamos a usar dql (data query language) que se usa para hacer consultas a la base de datos. Mirad en esta chuleta para conocer más.

Como SQL no hace distinción de mayúsculas y minúsculas usaré mayúsculas para las palabras clave de SQL y así las distinguiremos un poco mejor. Para hacer una petición se usa SELECT. Veamos como es una consulta para conocer las tablas que existen en la base de datos (esta consulta es específica para SQLite):

# Esto es específico para sqlite
print(pd.read_sql("SELECT name FROM sqlite_master WHERE type='table';", conn))
# Para otras BBDD puedes buscar en internet :-)
       name
0     datos
1  sensores
En la consulta anterior hemos usados varias palabras clave: SELECT, FROM, WHERE. Este tipo de consultas serán de lo más habitual. Vamos a explotar lo que ya sabemos.
Quiero los datos de todas las columnas de la tabla sensores (y pandas me lo meterá en un DataFrame, maravilloso!!!)

df = pd.read_sql("SELECT * FROM sensores;", conn)
print(df)
   index  precision sensores
0      0        0.1  marcaT1
1      1        0.5  marcaT2
2      2        1.0  marcaT3
3      3        2.0  marcaP1
4      4        5.0  marcaP2
Ahora queremos los datos de precipitación de Junio junto con su fecha. Fijaos que el valor superior del rango no es inclusivo (BETWEEN '2014-06-01' AND '2014-07-01' nos da el dato hasta antes de fecha = '2014-07-01'):

df = pd.read_sql("SELECT fecha, precipitacion FROM datos WHERE fecha BETWEEN '2014-06-01' AND '2014-07-01';", conn)
print(df)
                  fecha  precipitacion
0   2014-06-01 00:00:00      21.090544
1   2014-06-02 00:00:00      19.514893
2   2014-06-03 00:00:00       1.356933
3   2014-06-04 00:00:00      14.466592
4   2014-06-05 00:00:00      12.780801
5   2014-06-06 00:00:00      28.293163
6   2014-06-07 00:00:00       7.841272
7   2014-06-08 00:00:00      22.278980
8   2014-06-09 00:00:00      24.386602
9   2014-06-10 00:00:00       4.158349
10  2014-06-11 00:00:00       8.850627
11  2014-06-12 00:00:00      24.572156
12  2014-06-13 00:00:00      30.675862
13  2014-06-14 00:00:00       5.986650
14  2014-06-15 00:00:00      19.186014
15  2014-06-16 00:00:00      36.980785
16  2014-06-17 00:00:00      11.822683
17  2014-06-18 00:00:00       1.774325
18  2014-06-19 00:00:00       8.043394
19  2014-06-20 00:00:00       9.909083
20  2014-06-21 00:00:00      26.050138
21  2014-06-22 00:00:00      32.829371
22  2014-06-23 00:00:00       1.067285
23  2014-06-24 00:00:00      41.178811
24  2014-06-25 00:00:00       4.357684
25  2014-06-26 00:00:00      36.863093
26  2014-06-27 00:00:00      18.923454
27  2014-06-28 00:00:00       5.641863
28  2014-06-29 00:00:00       5.831242
29  2014-06-30 00:00:00      45.159804
Ahora quiero los datos de temperatura de los sensores con una precisión superior a 0.5 (el sensor marcaT1 es el único que me da precisión superior a 0.5):

df = pd.read_sql("SELECT fecha, tmedia FROM datos WHERE datos.sensorT = 'marcaT1';", conn)
print(df)
                   fecha     tmedia
0    2014-01-01 00:00:00  -5.981482
1    2014-01-08 00:00:00  -9.733159
2    2014-01-09 00:00:00 -12.674186
3    2014-01-11 00:00:00  -5.538770
4    2014-01-13 00:00:00  -5.380197
5    2014-01-15 00:00:00 -13.198654
6    2014-01-17 00:00:00 -12.994357
7    2014-01-19 00:00:00  -8.767548
8    2014-01-31 00:00:00  -8.950818
9    2014-02-02 00:00:00  -4.575402
10   2014-02-03 00:00:00  -7.380298
11   2014-02-09 00:00:00  -4.109866
12   2014-02-10 00:00:00  -7.882079
13   2014-02-13 00:00:00  -8.172534
14   2014-02-16 00:00:00  -1.425199
15   2014-02-18 00:00:00  -0.906962
16   2014-02-20 00:00:00  -7.373428
17   2014-02-24 00:00:00  -1.263014
18   2014-02-27 00:00:00  -3.394876
19   2014-03-05 00:00:00  -1.577839
20   2014-03-07 00:00:00   4.049144
21   2014-03-08 00:00:00   0.366216
22   2014-03-20 00:00:00   3.320033
23   2014-03-24 00:00:00  10.681308
24   2014-03-28 00:00:00  11.053572
25   2014-03-29 00:00:00   5.397594
26   2014-03-31 00:00:00  14.086071
27   2014-04-08 00:00:00  15.830806
28   2014-04-15 00:00:00  16.190236
29   2014-04-18 00:00:00  11.180340
..                   ...        ...
94   2014-10-11 00:00:00   6.982189
95   2014-10-12 00:00:00   6.645535
96   2014-10-13 00:00:00   3.310172
97   2014-10-16 00:00:00   8.312816
98   2014-10-18 00:00:00   4.656174
99   2014-10-20 00:00:00  -1.992949
100  2014-10-22 00:00:00   3.366216
101  2014-10-30 00:00:00   1.900779
102  2014-11-04 00:00:00  -6.545832
103  2014-11-08 00:00:00  -4.642068
104  2014-11-11 00:00:00   0.574801
105  2014-11-15 00:00:00  -5.413343
106  2014-11-18 00:00:00  -8.109866
107  2014-11-21 00:00:00  -2.766101
108  2014-11-22 00:00:00  -4.975587
109  2014-11-25 00:00:00  -6.575402
110  2014-11-28 00:00:00  -9.131020
111  2014-11-30 00:00:00  -5.476142
112  2014-12-03 00:00:00  -5.954868
113  2014-12-10 00:00:00  -4.883750
114  2014-12-11 00:00:00 -13.994357
115  2014-12-12 00:00:00  -8.099335
116  2014-12-14 00:00:00 -13.292284
117  2014-12-16 00:00:00  -8.462367
118  2014-12-18 00:00:00  -8.609383
119  2014-12-21 00:00:00 -13.786284
120  2014-12-23 00:00:00 -12.874932
121  2014-12-25 00:00:00  -7.940023
122  2014-12-26 00:00:00 -11.963711
123  2014-12-27 00:00:00  -8.981482

[124 rows x 2 columns]
Si os fijáis, en la consulta anterior he usado datos.sensorT (tabla.columna). Esto solo será necesario si estamos trabajando con varias tablas y una columna se puede llamar igual en varias tablas. De esta forma no hay posibilidad de equivocarse ya que usamos 'nombres y apellidos'.
Vamos a hacer una consulta que nos dé el mismo resultado pero que enlaza varias consultas (un SELECT dentro de un SELECT). También la represento de forma un poco más estructurada para que sea más legible:

# La misma consulta de antes pero sin saber la precisión de cada uno de los sensores a priori
q = """
SELECT 
    fecha, tmedia 
FROM 
    datos 
WHERE 
    datos.sensorT = 
    (SELECT 
        sensores 
    FROM 
        sensores 
    WHERE 
        precision < 0.5);"""

df = pd.read_sql(q, conn)
print(df)
                   fecha     tmedia
0    2014-01-01 00:00:00  -5.981482
1    2014-01-08 00:00:00  -9.733159
2    2014-01-09 00:00:00 -12.674186
3    2014-01-11 00:00:00  -5.538770
4    2014-01-13 00:00:00  -5.380197
5    2014-01-15 00:00:00 -13.198654
6    2014-01-17 00:00:00 -12.994357
7    2014-01-19 00:00:00  -8.767548
8    2014-01-31 00:00:00  -8.950818
9    2014-02-02 00:00:00  -4.575402
10   2014-02-03 00:00:00  -7.380298
11   2014-02-09 00:00:00  -4.109866
12   2014-02-10 00:00:00  -7.882079
13   2014-02-13 00:00:00  -8.172534
14   2014-02-16 00:00:00  -1.425199
15   2014-02-18 00:00:00  -0.906962
16   2014-02-20 00:00:00  -7.373428
17   2014-02-24 00:00:00  -1.263014
18   2014-02-27 00:00:00  -3.394876
19   2014-03-05 00:00:00  -1.577839
20   2014-03-07 00:00:00   4.049144
21   2014-03-08 00:00:00   0.366216
22   2014-03-20 00:00:00   3.320033
23   2014-03-24 00:00:00  10.681308
24   2014-03-28 00:00:00  11.053572
25   2014-03-29 00:00:00   5.397594
26   2014-03-31 00:00:00  14.086071
27   2014-04-08 00:00:00  15.830806
28   2014-04-15 00:00:00  16.190236
29   2014-04-18 00:00:00  11.180340
..                   ...        ...
94   2014-10-11 00:00:00   6.982189
95   2014-10-12 00:00:00   6.645535
96   2014-10-13 00:00:00   3.310172
97   2014-10-16 00:00:00   8.312816
98   2014-10-18 00:00:00   4.656174
99   2014-10-20 00:00:00  -1.992949
100  2014-10-22 00:00:00   3.366216
101  2014-10-30 00:00:00   1.900779
102  2014-11-04 00:00:00  -6.545832
103  2014-11-08 00:00:00  -4.642068
104  2014-11-11 00:00:00   0.574801
105  2014-11-15 00:00:00  -5.413343
106  2014-11-18 00:00:00  -8.109866
107  2014-11-21 00:00:00  -2.766101
108  2014-11-22 00:00:00  -4.975587
109  2014-11-25 00:00:00  -6.575402
110  2014-11-28 00:00:00  -9.131020
111  2014-11-30 00:00:00  -5.476142
112  2014-12-03 00:00:00  -5.954868
113  2014-12-10 00:00:00  -4.883750
114  2014-12-11 00:00:00 -13.994357
115  2014-12-12 00:00:00  -8.099335
116  2014-12-14 00:00:00 -13.292284
117  2014-12-16 00:00:00  -8.462367
118  2014-12-18 00:00:00  -8.609383
119  2014-12-21 00:00:00 -13.786284
120  2014-12-23 00:00:00 -12.874932
121  2014-12-25 00:00:00  -7.940023
122  2014-12-26 00:00:00 -11.963711
123  2014-12-27 00:00:00  -8.981482

[124 rows x 2 columns]
Podemos decirle que nos pase solo una serie de valores. Por ejemplo, solo quiero los tres valores más altos de precipitación de diciembre:

q = """
SELECT 
    fecha, precipitacion 
FROM 
    datos 
WHERE 
    fecha > '2014-11-30'
ORDER BY
    precipitacion DESC
LIMIT
    3"""

df = pd.read_sql(q, conn)
print(df)
                 fecha  precipitacion
0  2014-12-31 00:00:00      50.047120
1  2014-12-01 00:00:00      43.913895
2  2014-12-04 00:00:00      37.344830
En la consulta anterior le hemos pedido que nos ordenase por los valores de precipitación de forma descendente (es decir, por el final) y le hemos pedido que nos limitase la búsqueda a tres valores, los tres valores más altos.

¿Os ha picado el gusanillo?

Todo esto era para ver si os picaba un poco el gusanillo y dejáis (dejamos) de usar tanto fichero de texto/excel/csv y usamos opciones más ricas y potentes que existen por ahí.

Enlaces

Y cerramos la conexión (literal) por hoy.

conn.close() # :-)

Y el notebook en el repositorio, como siempre.

Pandas (VI)

Y mucho más

Esto solo ha sido un pequeño vistazo con cosas que considero importantes pero que no tienen que ser las más importantes. Podéis echarle un ojo a:

  • sort, max, min, head, tail, unique, groupby, apply, transform, stack, unstack, mean, std, isnull, value_counts, notnull, rank, dropna, fillna, describe, cov, corr, duplicated, drop, pivot, pivot_table, drop_duplicates, quantile,...

para seguir viendo cosas útiles.

Finalmente, después de haceros sufrir con el formateo del código dentro del wordpress os he dejado un notebook en el github de Pybonacci donde tenéis todo lo que hemos visto en esta serie además de un pequeño caso práctico de aplicación.

Pandas (V)

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 seguimos con esta quinta parte de la serie.

Unir (merge/join)

Pandas dispone de la función merge (documentación oficial) que permite 'unir' datos al estilo de como se hace con bases de datos relacionales (usando SQL). También se puede acceder al método merge disponible en las instancias a un Dataframe.

Por su parte, join es un método disponible en un DataFrame y sirve para hacer uniones de índices sobre índices o de índices sobre columnas. Las uniones que hace join las hace sobre los índices, en lugar de hacerlo sobre columnas comunes como se hace con merge. A ver si viendo los ejemplos queda un poco mejor este último párrafo y las diferencias entre join y merge.

Las uniones pueden ser uno-a-uno, muchos-a-uno o muchos-a-muchos.

Una unión uno-a-uno sería cuando unimos dos tablas (DataFrames) con índices únicos como hemos hecho en la entrega anterior con las concatenaciones.

datos1 = pd.DataFrame(np.random.randn(10), columns = ['columna1'])
datos2 = pd.DataFrame(np.random.randn(14), columns = ['columna2'], index = np.arange(1,15))
datos1j = datos1.join(datos2)
datos2j = datos2.join(datos1)
print('datos1j n{}n'.format(datos1j))
print('datos2j n{}'.format(datos2j))
datos1j 
   columna1  columna2
0 -0.209303       NaN
1 -0.430892  1.052453
2  0.766200 -0.346896
3  1.773694 -0.249700
4 -2.259187 -0.588739
5 -0.930647  0.160590
6  0.029990  0.421446
7  0.812770 -0.315913
8  0.681786  0.256745
9 -0.115109  0.524278

[10 rows x 2 columns]

datos2j 
    columna2  columna1
1   1.052453 -0.430892
2  -0.346896  0.766200
3  -0.249700  1.773694
4  -0.588739 -2.259187
5   0.160590 -0.930647
6   0.421446  0.029990
7  -0.315913  0.812770
8   0.256745  0.681786
9   0.524278 -0.115109
10 -1.707269       NaN
11 -1.140342       NaN
12 -1.751337       NaN
13 -0.481319       NaN
14  1.604800       NaN

[14 rows x 2 columns]

 

En los anteriores ejemplos, datos1j es el resultado de unir los datos datos2 a los datos datos1 en todos los índices comunes que tienen ambos teniendo solo en cuenta el rango de índices definido en datos1. Si algún dato en datos2 no tiene un índice presente en datos1 se rellenará con un NaN. Con datos2j sucede lo mismo que con datos1j lo que el índice que tiene relevancia ahora es el perteneciente a datos2j. No sé si habrá quedado más o menos claro.

Ahora vamos a unir pero usando la palabra clave how que nos permite decir como se van a tener en cuenta los índices. Normalmente le pasaremos el parámetro outer o inner. El primero, outer, indica que los índices de los DataFrames se unen como en una unión de conjuntos, el segundo, inner, une los índices como si hiciéramos una intersección de conjuntos. Veamos un par de ejemplos para que se vea de forma práctica, el primero usando outer y el segundo usando inner:

datos3j1 = datos1.join(datos2, how = 'outer')
datos3j2 = datos2.join(datos1, how = 'outer')
print('datos3j1 n{}n'.format(datos3j1))
print('datos3j2 recolocadosn{}n'.format(datos3j2.ix[:, ['columna1','columna2']]))
print('datos3j2 n{}'.format(datos3j2))
datos3j1 
    columna1  columna2
0  -0.209303       NaN
1  -0.430892  1.052453
2   0.766200 -0.346896
3   1.773694 -0.249700
4  -2.259187 -0.588739
5  -0.930647  0.160590
6   0.029990  0.421446
7   0.812770 -0.315913
8   0.681786  0.256745
9  -0.115109  0.524278
10       NaN -1.707269
11       NaN -1.140342
12       NaN -1.751337
13       NaN -0.481319
14       NaN  1.604800

[15 rows x 2 columns]

datos3j2 recolocados
    columna1  columna2
0  -0.209303       NaN
1  -0.430892  1.052453
2   0.766200 -0.346896
3   1.773694 -0.249700
4  -2.259187 -0.588739
5  -0.930647  0.160590
6   0.029990  0.421446
7   0.812770 -0.315913
8   0.681786  0.256745
9  -0.115109  0.524278
10       NaN -1.707269
11       NaN -1.140342
12       NaN -1.751337
13       NaN -0.481319
14       NaN  1.604800

[15 rows x 2 columns]

datos3j2 
    columna2  columna1
0        NaN -0.209303
1   1.052453 -0.430892
2  -0.346896  0.766200
3  -0.249700  1.773694
4  -0.588739 -2.259187
5   0.160590 -0.930647
6   0.421446  0.029990
7  -0.315913  0.812770
8   0.256745  0.681786
9   0.524278 -0.115109
10 -1.707269       NaN
11 -1.140342       NaN
12 -1.751337       NaN
13 -0.481319       NaN
14  1.604800       NaN

[15 rows x 2 columns]
datos4j1 = datos1.join(datos2, how = 'inner')
datos4j2 = datos2.join(datos1, how = 'inner')
print('datos4j1 n{}n'.format(datos4j1))
print('datos4j2 recolocadosn{}n'.format(datos4j2.ix[:, ['columna1','columna2']]))
print('datos4j2 n{}'.format(datos4j2))
datos4j1 
   columna1  columna2
1 -0.430892  1.052453
2  0.766200 -0.346896
3  1.773694 -0.249700
4 -2.259187 -0.588739
5 -0.930647  0.160590
6  0.029990  0.421446
7  0.812770 -0.315913
8  0.681786  0.256745
9 -0.115109  0.524278

[9 rows x 2 columns]

datos4j2 recolocados
   columna1  columna2
1 -0.430892  1.052453
2  0.766200 -0.346896
3  1.773694 -0.249700
4 -2.259187 -0.588739
5 -0.930647  0.160590
6  0.029990  0.421446
7  0.812770 -0.315913
8  0.681786  0.256745
9 -0.115109  0.524278

[9 rows x 2 columns]

datos4j2 
   columna2  columna1
1  1.052453 -0.430892
2 -0.346896  0.766200
3 -0.249700  1.773694
4 -0.588739 -2.259187
5  0.160590 -0.930647
6  0.421446  0.029990
7 -0.315913  0.812770
8  0.256745  0.681786
9  0.524278 -0.115109

[9 rows x 2 columns]

 

Todo lo anterior se puede hacer también usando la función o método merge pero encuentro que es una forma un poco más rebuscada por lo que no la vamos a mostrar aquí ya que añade complejidad. Veremos usos de merge más adelante.

Ahora vamos a mostrar una unión muchos-a-uno. Estas uniones se hacen sobre una o más columnas como referencia, no a partir de índices, por lo que los valores contenidos pueden no ser únicos. Como siempre, vamos a ver un poco de código para ver si clarifica un poco más la teoría:

datos1 = pd.DataFrame(np.random.randn(10), columns = ['columna1'])
datos1['otra_columna'] = ['hola', 'mundo'] * 5
datos2 = pd.DataFrame(np.random.randn(2,2), columns = ['col1', 'col2'], index = ['hola', 'mundo'])
print('datos1 n {} n'.format(datos1))
print('datos2 n {} n'.format(datos2))
print(u'Unión de datos n {} n'.format(datos1.join(datos2, on = 'otra_columna')))
datos1 
    columna1 otra_columna
0 -2.086230         hola
1 -1.015736        mundo
2 -0.919460         hola
3  0.923531        mundo
4 -0.445977         hola
5  0.719787        mundo
6  1.064480         hola
7 -0.235803        mundo
8  1.395844         hola
9  1.492875        mundo

[10 rows x 2 columns] 

datos2 
            col1      col2
hola   0.400267 -0.678126
mundo  0.855735  0.619193

[2 rows x 2 columns] 

Unión de datos 
    columna1 otra_columna      col1      col2
0 -2.086230         hola  0.400267 -0.678126
1 -1.015736        mundo  0.855735  0.619193
2 -0.919460         hola  0.400267 -0.678126
3  0.923531        mundo  0.855735  0.619193
4 -0.445977         hola  0.400267 -0.678126
5  0.719787        mundo  0.855735  0.619193
6  1.064480         hola  0.400267 -0.678126
7 -0.235803        mundo  0.855735  0.619193
8  1.395844         hola  0.400267 -0.678126
9  1.492875        mundo  0.855735  0.619193

[10 rows x 4 columns]

 

Estamos uniendo sobre los valores de la columna del DataFrame datos1 que presenta valores presentes en los índices del DataFrame datos2. En el anterior ejemplo hemos unido teniendo en cuenta una única columna, si queremos unir teniendo en cuenta varias columnas, el DataFrame que se le pase deberá presentar un MultiÍndice con tantos índices como columnas usemos (ver documentación sobre MultiÍndices y sobre unión con ellos).

Para hacer uniones de muchos-a-muchos usaremos merge que ofrece mayor libertad para poder hacer uniones de cualquier tipo (también las que hemos visto hasta ahora de uno-a-uno y de muchos-a-uno).

En el siguiente ejemplo vamos a hacer una unión de dos DataFrames usando merge y luego iremos explicando lo que hemos estado haciendo poco a poco para ver si se entiende un poco mejor.

datos_dcha = pd.DataFrame({'clave': ['foo'] * 3, 'valor_dcha': np.arange(3)})
datos_izda = pd.DataFrame({'clave': ['foo'] * 3, 'valor_izda': np.arange(5, 8)})
datos_unidos = pd.merge(datos_izda, datos_dcha, on = 'clave')
print('datos_dcha n {} n'.format(datos_dcha))
print('datos_izda n {} n'.format(datos_izda))
print('datos_unidos n {}'.format(datos_unidos))
datos_dcha 
   clave  valor_dcha
0   foo           0
1   foo           1
2   foo           2

[3 rows x 2 columns] 

datos_izda 
   clave  valor_izda
0   foo           5
1   foo           6
2   foo           7

[3 rows x 2 columns] 

datos_unidos 
   clave  valor_izda  valor_dcha
0   foo           5           0
1   foo           5           1
2   foo           5           2
3   foo           6           0
4   foo           6           1
5   foo           6           2
6   foo           7           0
7   foo           7           1
8   foo           7           2

[9 rows x 3 columns]

 

Vemos que si hacemos una unión de la anterior forma, a cada valor de datos_dcha le 'asocia' cada uno de los valores de datos_izda que tengan la misma clave. En la siquiente celda de código vemos otro ejemplo de lo anterior un poco más completo teniendo en cuenta dos columnas de claves y usando el método outer de 'unión':

datos_dcha = pd.DataFrame({'clave1': ['foo', 'foo', 'bar', 'bar'],
                           'clave2': ['one', 'one', 'one', 'two'],
                           'val_dcha': [4, 5, 6, 7]})
datos_izda = pd.DataFrame({'clave1': ['foo', 'foo', 'bar'],
                           'clave2': ['one', 'two', 'one'],
                           'val_izda': [1, 2, 3]})
datos_unidos = pd.merge(datos_izda, datos_dcha, how='outer')
print('datos_dcha n {} n'.format(datos_dcha))
print('datos_izda n {} n'.format(datos_izda))
print('datos_unidos n {}'.format(datos_unidos))
datos_dcha 
   clave1 clave2  val_dcha
0    foo    one         4
1    foo    one         5
2    bar    one         6
3    bar    two         7

[4 rows x 3 columns] 

datos_izda 
   clave1 clave2  val_izda
0    foo    one         1
1    foo    two         2
2    bar    one         3

[3 rows x 3 columns] 

datos_unidos 
   clave1 clave2  val_izda  val_dcha
0    foo    one         1         4
1    foo    one         1         5
2    foo    two         2       NaN
3    bar    one         3         6
4    bar    two       NaN         7

[5 rows x 4 columns]

 

Otra vez hemos llegado al final. ¡¡Estad atentos a la última entrega!!

Pandas (IV)

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 seguimos con esta cuarta parte de la serie.

Concatenando datos

Para concatenar ficheros se usa la función pd.concat (documentación oficial]. Un ejemplo rápido sería el siguiente:

datos1 = pd.DataFrame(np.random.randn(5,3))
datos2 = pd.DataFrame(np.random.randn(5,3))
piezas = [datos1, datos2]
datos_concatenados_a = pd.concat(piezas)
print('datos1n {}'.format(datos1))
print('datos2n {}'.format(datos2))
print('datos_concatenadosn {}'.format(datos_concatenados_a))

Cuyo resultado sería:

datos1
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786

[5 rows x 3 columns]
datos2
          0         1         2
0 -2.063044  2.328388  0.043275
1 -1.720170 -0.039871  0.954244
2 -0.173751  0.047003 -0.979577
3 -0.293044  1.928332 -1.323554
4  0.705127  3.711652 -0.535096

[5 rows x 3 columns]
datos_concatenados
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786
0 -2.063044  2.328388  0.043275
1 -1.720170 -0.039871  0.954244
2 -0.173751  0.047003 -0.979577
3 -0.293044  1.928332 -1.323554
4  0.705127  3.711652 -0.535096

[10 rows x 3 columns]

Interesante, rápido y limpio, como me gusta. Pero, si nos fijamos, tenemos un problema con los índices ya que algunos están repetidos. Si accedemos al índice 0, por ejemplo, obtendríamos dos filas de valores en lugar de una.

datos_concatenados_a.ix[0]
              0            1            2
0     -1.691985    -1.181241    -0.714437
0     -2.063044     2.328388     0.043275

2 rows × 3 columns

Lo anterior podría llevar a equívocos. Esto lo podemos solventar de varias formas. Una sería reescribiendo la columna de índices para que no haya malentendidos al hacer cualquier operación. Por ejemplo:

datos_concatenados_aa = datos_concatenados_a
datos_concatenados_aa.index = range(datos_concatenados_aa.shape[0])
print('datos_concatenadosn {}'.format(datos_concatenados_aa))
datos_concatenados
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786
5 -2.063044  2.328388  0.043275
6 -1.720170 -0.039871  0.954244
7 -0.173751  0.047003 -0.979577
8 -0.293044  1.928332 -1.323554
9  0.705127  3.711652 -0.535096

[10 rows x 3 columns]

O usando la palabra clave ignore_index pasándole el valor True al crear la concatenación. Por ejemplo:

datos_concatenados_aa = pd.concat(piezas, ignore_index = True)
print(datos_concatenados_aa)
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786
5 -2.063044  2.328388  0.043275
6 -1.720170 -0.039871  0.954244
7 -0.173751  0.047003 -0.979577
8 -0.293044  1.928332 -1.323554
9  0.705127  3.711652 -0.535096

[10 rows x 3 columns]

Vale, hemos solventado el anterior problema pero que pasa si, por la razón que sea, nos interesase conservar los índices originales. Podríamos usar palabras clave para cada 'cosa' concatenada en el DataFrame final. Ejemplo:

#datos1 = pd.DataFrame(np.random.randn(5,3))
#datos2 = pd.DataFrame(np.random.randn(5,3))
#piezas = [datos1, datos2]
datos_concatenados_b = pd.concat(piezas, keys = ['datos1', 'datos2'])
print('datos1n {}'.format(datos1))
print('datos2n {}'.format(datos2))
print('datos_concatenadosn {}'.format(datos_concatenados_b))
datos1
          0         1         2
0 -1.691985 -1.181241 -0.714437
1  0.955094 -0.238498  1.137918
2 -0.533739 -0.285976 -0.990184
3 -0.626446  0.664830  0.278803
4 -0.183818 -0.013190  0.505786

[5 rows x 3 columns]
datos2
          0         1         2
0 -2.063044  2.328388  0.043275
1 -1.720170 -0.039871  0.954244
2 -0.173751  0.047003 -0.979577
3 -0.293044  1.928332 -1.323554
4  0.705127  3.711652 -0.535096

[5 rows x 3 columns]
datos_concatenados
                 0         1         2
datos1 0 -1.691985 -1.181241 -0.714437
       1  0.955094 -0.238498  1.137918
       2 -0.533739 -0.285976 -0.990184
       3 -0.626446  0.664830  0.278803
       4 -0.183818 -0.013190  0.505786
datos2 0 -2.063044  2.328388  0.043275
       1 -1.720170 -0.039871  0.954244
       2 -0.173751  0.047003 -0.979577
       3 -0.293044  1.928332 -1.323554
       4  0.705127  3.711652 -0.535096

[10 rows x 3 columns]

Vemos que hay índices repetidos pero están en 'grupos' diferentes. De esta forma, si queremos acceder a la fila con índice 0 del primer grupo de datos concatenados (datos1) podemos hacer lo siguiente:

print(datos_concatenados_b.ix['datos1'].ix[0])
<pre>0   -1.691985
1   -1.181241
2   -0.714437
Name: 0, dtype: float64</pre>
Estamos viendo filas, pero podemos hacer los mismo para las columnas, por supuesto, usando el nombre de la columna (en el ejemplo siguiente, la columna 0):
[code language="Python"]
print(datos_concatenados_b.ix['datos1'][0])
0   -1.691985
1    0.955094
2   -0.533739
3   -0.626446
4   -0.183818
Name: 0, dtype: float64

Vemos qué tipo de índice es este índice 'compuesto' que hemos creado:

datos_concatenados_b.index
MultiIndex(levels=[['datos1', 'datos2'], [0, 1, 2, 3, 4]],
labels=[[0, 0, 0, 0, 0, 1, 1, 1, 1, 1], [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]])

Vemos que es un MultiIndex. No vamos a ver mucho más pero os lo dejo anotado para que sepáis que existen combinaciones de índices (o de columnas) y se manejan de forma un poco más compleja que un índice 'simple'. Se conoce como indexación jerárquica y permiten ser un poco más descriptivos (verbose) con nuestros DataFrames aunque conlleva un punto más de complejidad a la hora de trabajar con los datos.

¿Qué pasa cuando una de las columnas no es igual en los grupos de datos que queramos concatenar? El nuevo DataFrame tendrá en cuenta este aspecto rellenando con NaNs donde convenga. Veamos el siguiente código de ejemplo:

datos1 = pd.DataFrame(np.random.randn(5,3))
datos2 = pd.DataFrame(np.random.randn(5,4))
piezas = [datos1, datos2]
datos_concatenados_c = pd.concat(piezas, ignore_index = True)
print('datos1n {}'.format(datos1))
print('datos2n {}'.format(datos2))
print('datos_concatenadosn {}'.format(datos_concatenados_c))
datos1
          0         1         2
0 -0.082729 -0.016452 -1.280156
1  0.606336 -0.504770 -2.017690
2 -2.147009 -0.632275  0.023689
3 -0.255461 -0.042007  0.661835
4  2.351576  0.735611 -0.187072

[5 rows x 3 columns]
datos2
          0         1         2         3
0 -0.223023  0.070622 -0.577119 -1.430177
1 -1.661289 -0.214221  0.709818 -0.642611
2 -0.098368 -0.489105 -1.373906 -2.104431
3  0.880578 -0.601151 -1.450542 -0.289738
4 -1.461346 -0.539262  0.327825 -0.944431

[5 rows x 4 columns]
datos_concatenados
          0         1         2         3
0 -0.082729 -0.016452 -1.280156       NaN
1  0.606336 -0.504770 -2.017690       NaN
2 -2.147009 -0.632275  0.023689       NaN
3 -0.255461 -0.042007  0.661835       NaN
4  2.351576  0.735611 -0.187072       NaN
5 -0.223023  0.070622 -0.577119 -1.430177
6 -1.661289 -0.214221  0.709818 -0.642611
7 -0.098368 -0.489105 -1.373906 -2.104431
8  0.880578 -0.601151 -1.450542 -0.289738
9 -1.461346 -0.539262  0.327825 -0.944431

[10 rows x 4 columns]

Vemos que el primer grupo de datos, datos1, solo tiene tres columnas mientras que el segundo grupo, datos2, tiene 4 columnas. El resultado final tendrá en cuenta esto y rellenerá la columna 3 que pertenece a los datos del primer grupo de datos, datos1. Cool!

Lo visto hasta ahora para concatenar Series o DataFrames lo podemos hacer también usando el método append. Veamos un ejemplo similar a lo anterior:

datos1 = pd.DataFrame(np.random.randn(5,3))
datos2 = pd.DataFrame(np.random.randn(5,4))
datos_concatenados_d = datos1.append(datos2, ignore_index = True)
print('datos1n {}'.format(datos1))
print('datos2n {}'.format(datos2))
print('datos_concatenadosn {}'.format(datos_concatenados_d))
datos1
          0         1         2
0 -0.974367  1.732370  0.354479
1 -0.021746  2.215287  1.107243
2  0.018506  1.301015  1.103651
3 -1.857281 -1.181981  0.097104
4 -0.595689  0.140885  1.993213

[5 rows x 3 columns]
datos2
          0         1         2         3
0 -0.211180 -0.093403  0.215210 -0.154284
1  0.206997  1.277379 -0.893895 -0.216731
2 -1.138390 -0.067240  1.688928 -2.191215
3  0.938069  0.174496 -1.722735 -0.873746
4  0.177425  0.823896 -0.595673 -0.426416

[5 rows x 4 columns]
datos_concatenados
          0         1         2         3
0 -0.974367  1.732370  0.354479       NaN
1 -0.021746  2.215287  1.107243       NaN
2  0.018506  1.301015  1.103651       NaN
3 -1.857281 -1.181981  0.097104       NaN
4 -0.595689  0.140885  1.993213       NaN
5 -0.211180 -0.093403  0.215210 -0.154284
6  0.206997  1.277379 -0.893895 -0.216731
7 -1.138390 -0.067240  1.688928 -2.191215
8  0.938069  0.174496 -1.722735 -0.873746
9  0.177425  0.823896 -0.595673 -0.426416

[10 rows x 4 columns]

Otra vez hemos llegado al final. ¡¡Estad atentos a la próxima entrega!!

Pandas (III)

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 seguimos con esta tercera parte de la serie.

Trabajando con datos, indexación, selección,...

¿Cómo podemos seleccionar, añadir, eliminar, mover,..., columnas, filas,...?

Para seleccionar una columna solo hemos de usar el nombre de la columna y pasarlo como si fuera un diccionario (o un atributo).

Para añadir una columna simplemente hemos de usar un nombre de columna no existente y pasarle los valores para esa columna.

Para eliminar una columna podemos usar del o el método pop del DataFrame.

Para mover una columna podemos usar una combinación de las metodologías anteriores.

Por ejemplo, vemos a seleccionar los valores de una columna:

df = pd.DataFrame(np.random.randn(5,3),
                       index = ['primero','segundo','tercero','cuarto','quinto'],
                       columns = ['velocidad', 'temperatura','presion'])
print(df['velocidad'])
print(df.velocidad)

Hemos creado un DataFrame y para acceder a la columna velocidad lo podemos hacer de dos formas. O bien usando el nombre de la columna como si fuera una clave de un diccionario o bien usando el nombre de la columna como si fuera un atributo. En el caso de que los nombres de las columnas sean números, la segunda opción no podríais usarla...

Vamos a añadir una columna nueva al DataFrame. Es algo tan sencillo como usar un nombre de columna no existente y pasarle los datos:

df['velocidad_maxima'] = np.random.randn(df.shape[0])
print(df)

Y el resultado sería:

         velocidad  temperatura   presion  velocidad_maxima
primero   0.175374     0.384571 -0.575126         -0.474630
segundo  -0.133466     0.987833  0.305844         -0.746577
tercero  -0.418224     0.603431  0.128822          1.545612
cuarto   -0.320517    -0.643183  0.319838          0.634203
quinto    0.955521    -0.295541 -1.277743          2.389485

[5 rows x 4 columns]

 

Pero qué pasa si quiero añadir la columna en un lugar específico. Para ello podemos usar el método insert (y de paso vemos como podemos borrar una columna):

# forma 1 (borramos la columna 'velocidad_maxima' que está al final del df usando del)
#         (Colocamos la columna eliminada en la posición que especifiquemos)
print(df)
columna = df['velocidad_maxima']
del df['velocidad_maxima']
print(df)
print(columna)
df.insert(1, 'velocidad_maxima', columna)
print(df)

El resultado del DataFrame paso a paso sería:

         velocidad  temperatura   presion  velocidad_maxima
primero   0.175374     0.384571 -0.575126         -0.474630
segundo  -0.133466     0.987833  0.305844         -0.746577
tercero  -0.418224     0.603431  0.128822          1.545612
cuarto   -0.320517    -0.643183  0.319838          0.634203
quinto    0.955521    -0.295541 -1.277743          2.389485

[5 rows x 4 columns]
         velocidad  temperatura   presion
primero   0.175374     0.384571 -0.575126
segundo  -0.133466     0.987833  0.305844
tercero  -0.418224     0.603431  0.128822
cuarto   -0.320517    -0.643183  0.319838
quinto    0.955521    -0.295541 -1.277743

[5 rows x 3 columns]
primero   -0.474630
segundo   -0.746577
tercero    1.545612
cuarto     0.634203
quinto     2.389485
Name: velocidad_maxima, dtype: float64
         velocidad  velocidad_maxima  temperatura   presion
primero   0.175374         -0.474630     0.384571 -0.575126
segundo  -0.133466         -0.746577     0.987833  0.305844
tercero  -0.418224          1.545612     0.603431  0.128822
cuarto   -0.320517          0.634203    -0.643183  0.319838
quinto    0.955521          2.389485    -0.295541 -1.277743

[5 rows x 4 columns]

 

Una forma alternativa sería usando el método pop:

# forma 2 (borramos usando el método pop y añadimos la columna borrada en la última posición de nuevo)
print(df)
columna = df.pop('velocidad_maxima')
print(df)
print(columna)
df.insert(3, 'velocidad_maxima', columna)
print(df)

Cuyo resultado, paso a paso sería:

         velocidad  velocidad_maxima  temperatura   presion
primero   0.175374         -0.474630     0.384571 -0.575126
segundo  -0.133466         -0.746577     0.987833  0.305844
tercero  -0.418224          1.545612     0.603431  0.128822
cuarto   -0.320517          0.634203    -0.643183  0.319838
quinto    0.955521          2.389485    -0.295541 -1.277743

[5 rows x 4 columns]
         velocidad  temperatura   presion
primero   0.175374     0.384571 -0.575126
segundo  -0.133466     0.987833  0.305844
tercero  -0.418224     0.603431  0.128822
cuarto   -0.320517    -0.643183  0.319838
quinto    0.955521    -0.295541 -1.277743

[5 rows x 3 columns]
primero   -0.474630
segundo   -0.746577
tercero    1.545612
cuarto     0.634203
quinto     2.389485
Name: velocidad_maxima, dtype: float64
         velocidad  temperatura   presion  velocidad_maxima
primero   0.175374     0.384571 -0.575126         -0.474630
segundo  -0.133466     0.987833  0.305844         -0.746577
tercero  -0.418224     0.603431  0.128822          1.545612
cuarto   -0.320517    -0.643183  0.319838          0.634203
quinto    0.955521    -0.295541 -1.277743          2.389485

[5 rows x 4 columns]

 

Para seleccionar datos concretos de un DataFrame podemos usar el índice, una rebanada, valores booleanos, la columna,...

print('Seleccionamos la columna de velocidades')
print(df['velocidad'])
Seleccionamos la columna de velocidades
primero    0.175374
segundo   -0.133466
tercero   -0.418224
cuarto    -0.320517
quinto     0.955521
Name: velocidad, dtype: float64
print('Seleccionamos todas las columnas cuyo índice es igual a tercero')
print(df.xs('tercero'))
Seleccionamos todas las columnas cuyo índice es igual a tercero
velocidad        -0.418224
temperatura       0.603431
presion           0.128822
velocidad_maxima  1.545612
Name: tercero, dtype: float64
print('Seleccionamos todas las columnas cuyo índice está entre tercero y quinto')
print('Daos cuenta que en este caso los índices son inclusivos')
print(df.ix['tercero':'quinto'])
Seleccionamos todas las columnas cuyo índice está entre tercero y quinto
Daos cuenta que en este caso los índices son inclusivos
         velocidad  temperatura   presion  velocidad_maxima
tercero  -0.418224     0.603431  0.128822          1.545612
cuarto   -0.320517    -0.643183  0.319838          0.634203
quinto    0.955521    -0.295541 -1.277743          2.389485

[3 rows x 4 columns]
print('Seleccionamos todos los valores de velocidad donde la temperatura > 0')
print(df[df['temperatura' > 0]['velocidad'])
Seleccionamos todos los valores de velocidad donde la temperatura > 0
primero    0.175374
segundo   -0.133466
tercero   -0.418224
Name: velocidad, dtype: float64
print('Seleccionamos todos los valores de una columna por índice usando una')
print('rebanada (slice) de enteros')
print('Daos cuenta que en este caso el límite superior de la rebanada no se')
print('incluye (Python tradicional)')
print(df.ix[1:3])
Seleccionamos todos los valores de una columna por índice usando una
rebanada (slice) de enteros
Daos cuenta que en este caso el límite superior de la rebanada no se
incluye (Python tradicional)
         velocidad  temperatura   presion  velocidad_maxima
segundo  -0.133466     0.987833  0.305844         -0.746577
tercero  -0.418224     0.603431  0.128822          1.545612

[2 rows x 4 columns]
print(u'Seleccionamos filas y columnas')
print(df.ix[1:3, ['velocidad', 'presion']])
Seleccionamos filas y columnas
         velocidad   presion
segundo  -0.133466  0.305844
tercero  -0.418224  0.128822

[2 rows x 2 columns]
# Algunas de las cosas anteriores se pueden realizar sin usar los métodos .ix() o .xs()</span>
print(df['velocidad]
segundo   -0.133466
tercero   -0.418224
Name: velocidad, dtype: float64
# Da igual si colocamos el slice primero y después las columnas:
df['velocidad'][1:3] == df[1:3]['velocidad']
segundo    True
tercero    True
Name: velocidad, dtype: bool

 

En lo anterior he estado usando los métodos .ix(), .xs() para obtener partes del DataFrame. Son herramientas muy flexibles que nos permiten acceder a los datos de forma muy personalizada. Otras opciones sería usar los métodos .loc(), .iloc(), .select(). Es importante tener en cuenta que las series devueltas cuando se indexa un DataFrame son solo vistas y no una copia de los propios datos. Por tanto, debes ser precavido cuando manipulas los datos (al igual que sucede con los numpy arrays y otros tipos de datos). Lo siguiente (hecho con numpy arrays) es equivalente para las estructuras de datos de Pandas.

# Vista, ¡Cuidado!
a = np.random.rand(5)
data = a[0:2]
data[:] = -999
print(a)
# Copias
a = np.random.rand(5)
data = a[0:2].copy()
data[:] = -999
print(a)
a = np.random.rand(5)
data = 1 * a[0:2]
data[:] = -999
print(a)
a = np.random.rand(5)
np.copyto(data, a[0:2]) # En este caso, data tiene que existir
data[:] = -999
print(a)
a = np.random.rand(5)
data = np.array(a[0:2])
data[:] = -999
print(a)
[ -9.99000000e+02  -9.99000000e+02   7.18723608e-01   5.30962716e-01
   3.43706883e-01]
[ 0.20812195  0.36386055  0.17570252  0.31071035  0.38838464]
[ 0.37175682  0.36962863  0.14481144  0.80786818  0.82803089]
[ 0.89958739  0.00190588  0.14769624  0.3378831   0.74536315]
[ 0.19285654  0.51489647  0.19612007  0.52342758  0.2006809 ]

 

Para acceder a los valores de los índices podemos usar .index.

df.index
Index(['primero', 'segundo', 'tercero', 'cuarto', 'quinto'], dtype='object')

 

Para acceder a los valores de las columnas podemos usar .columns.

df.columns
Index(['velocidad', 'temperatura', 'presion', 'velocidad_maxima'], dtype='object')

 

Otra vez hemos llegado al final. ¡¡Estad atentos a la próxima entrega!!

Pandas (II)

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 seguimos con esta segunda parte de la serie.

Leyendo y escribiendo datos (IO)

Una de las cosas que más me gusta de Pandas es la potencia que aporta a lo hora de leer y/o escribir ficheros de datos. Pandas es capaz de leer datos de ficheros csv, excel, HDF5, sql, json, html,...

Si trabajáis con datos de terceros, que pueden provenir de muy diversas fuentes, una de las partes más tediosas del trabajo será tener los datos listos para empezar a trabajar. Limpiar huecos, poner fechas en formato usable, saltarse cabeceros,...

Sin duda, una de las funciones que usaréis más será read_csv() que permite una gran flexibilidad a la hora de leer un fichero de texto plano.

Veamos la documentación:

Docstring:
Read CSV (comma-separated) file into DataFrame

Also supports optionally iterating or breaking of the file
into chunks.

Parameters
----------
filepath_or_buffer : string or file handle / StringIO. The string could be
    a URL. Valid URL schemes include http, ftp, s3, and file. For file URLs, a
    host is expected. For instance, a local file could be
    file ://localhost/path/to/table.csv
sep : string, default ','
    Delimiter to use. If sep is None, will try to automatically determine
    this. Regular expressions are accepted.

lineterminator : string (length 1), default None
    Character to break file into lines. Only valid with C parser
quotechar : string (length 1)
    The character used to denote the start and end of a quoted item. Quoted
    items can include the delimiter and it will be ignored.
quoting : int or csv.QUOTE_* instance, default None
    Control field quoting behavior per ``csv.QUOTE_*`` constants. Use one of
    QUOTE_MINIMAL (0), QUOTE_ALL (1), QUOTE_NONNUMERIC (2) or QUOTE_NONE (3).
    Default (None) results in QUOTE_MINIMAL behavior.
skipinitialspace : boolean, default False
    Skip spaces after delimiter
escapechar : string
dtype : Type name or dict of column -> type
    Data type for data or columns. E.g. {'a': np.float64, 'b': np.int32}
compression : {'gzip', 'bz2', None}, default None
    For on-the-fly decompression of on-disk data
dialect : string or csv.Dialect instance, default None
    If None defaults to Excel dialect. Ignored if sep longer than 1 char
    See csv.Dialect documentation for more details
header : int row number(s) to use as the column names, and the start of the
    data.  Defaults to 0 if no ``names`` passed, otherwise ``None``. Explicitly
    pass ``header=0`` to be able to replace existing names. The header can be
    a list of integers that specify row locations for a multi-index on the
    columns E.g. [0,1,3]. Intervening rows that are not specified will be
    skipped. (E.g. 2 in this example are skipped)
skiprows : list-like or integer
    Row numbers to skip (0-indexed) or number of rows to skip (int)
    at the start of the file
index_col : int or sequence or False, default None
    Column to use as the row labels of the DataFrame. If a sequence is given, a
    MultiIndex is used. If you have a malformed file with delimiters at the end
    of each line, you might consider index_col=False to force pandas to _not_
    use the first column as the index (row names)
names : array-like
    List of column names to use. If file contains no header row, then you
    should explicitly pass header=None
prefix : string or None (default)
    Prefix to add to column numbers when no header, e.g 'X' for X0, X1, ...
na_values : list-like or dict, default None
    Additional strings to recognize as NA/NaN. If dict passed, specific
    per-column NA values
true_values : list
    Values to consider as True
false_values : list
    Values to consider as False
keep_default_na : bool, default True
    If na_values are specified and keep_default_na is False the default NaN
    values are overridden, otherwise they're appended to
parse_dates : boolean, list of ints or names, list of lists, or dict
    If True -> try parsing the index.
    If [1, 2, 3] -> try parsing columns 1, 2, 3 each as a separate date column.
    If [[1, 3]] -> combine columns 1 and 3 and parse as a single date column.
    {'foo' : [1, 3]} -> parse columns 1, 3 as date and call result 'foo'
    A fast-path exists for iso8601-formatted dates.
keep_date_col : boolean, default False
    If True and parse_dates specifies combining multiple columns then
    keep the original columns.
date_parser : function
    Function to use for converting a sequence of string columns to an
    array of datetime instances. The default uses dateutil.parser.parser
    to do the conversion.
dayfirst : boolean, default False
    DD/MM format dates, international and European format
thousands : str, default None
    Thousands separator
comment : str, default None
    Indicates remainder of line should not be parsed
    Does not support line commenting (will return empty line)
decimal : str, default '.'
    Character to recognize as decimal point. E.g. use ',' for European data
nrows : int, default None
    Number of rows of file to read. Useful for reading pieces of large files
iterator : boolean, default False
    Return TextFileReader object
chunksize : int, default None
    Return TextFileReader object for iteration
skipfooter : int, default 0
    Number of line at bottom of file to skip
converters : dict. optional
    Dict of functions for converting values in certain columns. Keys can either
    be integers or column labels
verbose : boolean, default False
    Indicate number of NA values placed in non-numeric columns
delimiter : string, default None
    Alternative argument name for sep. Regular expressions are accepted.
encoding : string, default None
    Encoding to use for UTF when reading/writing (ex. 'utf-8')
squeeze : boolean, default False
    If the parsed data only contains one column then return a Series
na_filter: boolean, default True
    Detect missing value markers (empty strings and the value of na_values). In
    data without any NAs, passing na_filter=False can improve the performance
    of reading a large file
usecols : array-like
    Return a subset of the columns.
    Results in much faster parsing time and lower memory usage.
mangle_dupe_cols: boolean, default True
    Duplicate columns will be specified as 'X.0'...'X.N', rather than 'X'...'X'
tupleize_cols: boolean, default False
    Leave a list of tuples on columns as is (default is to convert to
    a Multi Index on the columns)
error_bad_lines: boolean, default True
    Lines with too many fields (e.g. a csv line with too many commas) will by
    default cause an exception to be raised, and no DataFrame will be returned.
    If False, then these "bad lines" will dropped from the DataFrame that is
    returned. (Only valid with C parser).
warn_bad_lines: boolean, default True
    If error_bad_lines is False, and warn_bad_lines is True, a warning for each
    "bad line" will be output. (Only valid with C parser).
infer_datetime_format : boolean, default False
    If True and parse_dates is enabled for a column, attempt to infer
    the datetime format to speed up the processing

Returns
-------
result : DataFrame or TextParser

 

Vamos a inventarnos un fichero de datos... (la siguiente pieza de código es específica de IPython y, por tanto, funciona solo en IPython)

%%writefile dummy.data
cabecero estúpido
901001 0000  7.54 -11.67  1.07  4.27
901001 0600 19.61 -2.74 27.87 -8.96
901001 1200 -4.34  0.73 -6.58  0.17
901001 1800 -4.99  3.24 10.62 -6.13
901002 0000 -3.54 10.39 -12.05 -13.35
901002 0600 12.55  3.80  4.92 -8.18
901002 1200 1.06 23.75 -8.03 -8.67
901002 1800 -1.12  1.82  7.09 -6.06
901003 0600 -5.90  2.38 19.33  6.84
901003 1200 -9.51 -2.72 -7.13 -0.35
901003 1800  6.49 -12.01 -13.62 -0.93

 

Vamos a leer con Pandas el fichero que acabamos de crear:

data = pd.read_csv('dummy.data', sep = 's*',
                   names = ['fecha', 'hora', 'rec1', 'rec2', 'rec3', 'rec4'],
                   skiprows = 1, parse_dates = [[0, 1]], index_col = 0)

 

Veamos lo que acabamos de leer:

print(data)

Y lo que nos mostrará será:

                      rec1   rec2   rec3   rec4
fecha_hora                                     
1990-10-01 00:00:00   7.54 -11.67   1.07   4.27
1990-10-01 06:00:00  19.61  -2.74  27.87  -8.96
1990-10-01 12:00:00  -4.34   0.73  -6.58   0.17
1990-10-01 18:00:00  -4.99   3.24  10.62  -6.13
1990-10-02 00:00:00  -3.54  10.39 -12.05 -13.35
1990-10-02 06:00:00  12.55   3.80   4.92  -8.18
1990-10-02 12:00:00   1.06  23.75  -8.03  -8.67
1990-10-02 18:00:00  -1.12   1.82   7.09  -6.06
1990-10-03 06:00:00  -5.90   2.38  19.33   6.84
1990-10-03 12:00:00  -9.51  -2.72  -7.13  -0.35
1990-10-03 18:00:00   6.49 -12.01 -13.62  -0.93

[11 rows x 4 columns]

 

Como veis, hemos usado:

  • una expresión regular en sep permitiéndonos mucha libertad a la hora de definir como están separados los datos. Si los delimitadores de los datos en cada fila son espacios o tabulaciones podríamos usar delim_whitespace, que es muchísimo más eficiente que usar expresiones regulares (AVISO: esto no aparece en la ayuda de la función read_csv).
  • names sirve para indicar qué nombres hay que poner a cada una de las columnas. Si no ponemos ningún nombre las nombrará con números empezando con el 0.
  • parse_dates es otra de las cosas realmente útiles cuando trabajamos con registros temporales. Ahí le indicamos qué columnas tiene que considerar como fechas y las 'parsea' para convertirlo en un tipo interno de fechas. Si os fijáis, he puesto las columnas 0 y 1 dentro de una lista, de esa forma las unirá en una sola columna de fechas con la que será más cómodo trabajar. Si automágicamente no es capaz de leer los formatos de las fechas le podemos indicar como las debe parsear (ver date_parser).
  • El último parámetro que le hemos pasado es index_col, le indicamos que la columna de índices será la 0, que será la unión de las columnas 0 y 1 de fechas.

Muy poderoso, ¿eh?

Escribir el resultado final en un fichero csv, por ejemplo, es algo tan sencillo como:

data.to_csv('dummy.csv')

Si queréis ver el resultado del fichero final creado y estáis en IPython podéis escribir lo siguiente:

%load dummy.csv

Y en pantalla veréis el siguiente texto:

fecha_hora,rec1,rec2,rec3,rec4
1990-10-01 00:00:00,7.54,-11.67,1.07,4.27
1990-10-01 06:00:00,19.61,-2.74,27.87,-8.96
1990-10-01 12:00:00,-4.34,0.73,-6.58,0.17
1990-10-01 18:00:00,-4.99,3.24,10.62,-6.13
1990-10-02 00:00:00,-3.54,10.39,-12.05,-13.35
1990-10-02 06:00:00,12.55,3.8,4.92,-8.18
1990-10-02 12:00:00,1.06,23.75,-8.03,-8.67
1990-10-02 18:00:00,-1.12,1.82,7.09,-6.06
1990-10-03 06:00:00,-5.9,2.38,19.33,6.84
1990-10-03 12:00:00,-9.51,-2.72,-7.13,-0.35
1990-10-03 18:00:00,6.49,-12.01,-13.62,-0.93

 

Otra tarea que se realiza habitualmente sería la de trabajar con información de una base de datos SQL. No lo vamos a ver aquí pero podéis ver este notebook donde se explica como leer y/o escribir datos de una BBDD SQL (SQLite, PostgreSQL o MySQL). Una vez que se han leído, el tratamiento es el mismo que si los hubiésemos leído de otro origen.

Es suficiente por hoy. Esta entrada ha sido cortita pero en breve dejaremos una tercera parte con más enjundia... ¡¡Estad atentos!!

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!!

Repaso a PyData 2013

Unos días después de la PyConUS 2013 se celebró la primera PyData del año (creo que serán semestrales de forma regular aunque el tiempo dirá). Entre las charlas había algunas introductorias, otras más avanzadas y otras enseñando aplicaciones prácticas.

Entre las charlas introductorias destacaremos:

Introducción a Numpy por Bryan Van De Ven: si no conoces absolutamente nada de Numpy esta es tu charla. Da un repaso por las cosas más frecuentes del uso de Numpy sin meterse en cosas muy esotéricas. Puedes sacar la libreta e ir apuntando las cosas que creas que te puedan resultar útiles para tus análisis.

Pandas por Wes McKinney: Es una charla introductoria. El problema que veo es que Pandas no es algo tan centrado como Numpy con su ndarray, las posibilidades de uso son múltiples y, quizá, hacer algo introductorio en vídeo sobre Pandas no resulte tan sencillo como  hacerlo con Numpy. En general, la documentación de Pandas es aceptable (aunque incompleta en algunos momentos) y la veo como un buen punto de partida antes de empezar a ver vídeos sobre Pandas. Creo que lo mejor para empezar con Pandas es echarle un ojo al tour de 10 minutos en vídeo o en texto) y luego empezar a trastear con la librería y con la documentación para empezar a entenderlo. Por nuestra parte, estamos preparando nuestro tutorial cuyos primeros capítulos estaran disponibles en breve, stay tuned!!!! En esta conferencia ha habido más vídeos sobre Pandas pero son más avanzados (primero para marujear con datos de forma productiva, segundo (con numpy y statsmodels) para análisis de series temporales) .

Aprendiendo Python por Peter Norvig: Otro tutorial más para empezar con Python!!

Hacer bonitos gráficos con MatPlotLib por Mike Müller: Otro más avanzado muestra como hacer MatPlotLib más interactivo gracias al gran Jake Vanderplas. Os dejamos aquí nuestro tutorial de matplotlib por si alguno no lo conoce aún (#autobombo).

Visualización de datos con NodeBox por Lynn Cherny: Librería para hacer gráficos más 'artísticos'. Yo tengo sentimientos encontrados con algunos enfoques de este tipo de gráficos (NodeBox, D3,...) por lo que te recomiendo mejor verlo y, si alguien quiere, lo discutimos en los comentarios.

Scikit-image por Davin Potts: Creo que esta librería es una de las grandes desconocidas y ofrece unas posibilidades muy interesantes. Si no la conoces deberías echarle un ojo al vídeo.

Entre las que hablan sobre cosas más prácticas y no específicamente de librerías destacaría (alguna no porque me haya gustado especialmente):

Análisis de redes sociales por Katherine Chuang: Estas están muy de moda (teoría de grafos) y están empezando a ser aburridas si no muestran algo excepcional o no sacan conclusiones **medibles** de todo el análisis chachiguay que hacen. Usa NetworkX, también muy de moda.

Plataforma de datos espacio temporales para el océano por André Karpistsenko: Esta me ha parecido interesante ya que muestra todo el pifostio de tecnologías y trabajo que hay detrás de muchas webs a las que voy a descargarme datos para mis análisis.

Hay más charlas avanzadas que hablan de HDF5 ([1]), Machine Learning ([1], [2], [3]), Blaze (el futuro de Numpy), IPython y más cosas del Big Data y herramientas Python para lidiar con ello.

Si le echáis un ojo a algún vídeo, por favor, dejad algún comentario más abajo para saber lo que os ha parecido.

Saludos y espero veros pronto entre esa gran cantidad de datos :-P