Acerca de Pybonacci:
import pandas as pd
import sys
import datetime as dt
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
%matplotlib inline
print("Versión de Python: ", sys.version)
print("Versión de Pandas: ", pd.version.short_version)
print("Versión de Numpy: ", np.version.short_version)
print("Versión de Matplotlib: ", matplotlib.__version__)
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
/home/kiko/pyprojs/venvs/vepy331/lib/python3.3/importlib/_bootstrap.py:313: UserWarning: Module readline was already imported from /home/kiko/pyprojs/venvs/vepy331/lib/python3.3/lib-dynload/readline.cpython-33m.so, but /home/kiko/pyprojs/venvs/vepy331/lib/python3.3/site-packages is being added to sys.path return f(*args, **kwds)
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:
from IPython.core.display import HTML
HTML("<iframe src=http://pandas.pydata.org width=800 height=350></iframe>")
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
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))
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))
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))
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))
#...
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{}\n\n'.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 serie\n{}".format('a' in serie))
print(">>> 'z' in serie\n{}".format('z' in serie))
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']
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
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:
numpy
arrays de 1D, diccionarios de listas, diccionarios de diccionarios o diccionarios de Series.numpy
array de 2Dnumpy
array estructurado (structured ndarray o record ndarray)Series
de PandasDataFrame
de PandasAdemá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))
#...
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
.
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:
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...
%%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
Overwriting dummy.data
data = pd.read_csv('dummy.data', sep = '\s*',
names = ['fecha', 'hora', 'rec1', 'rec2', 'rec3', 'rec4'],
skiprows = 1, parse_dates = [[0, 1]], index_col = 0)
print(data)
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]
data.index
<class 'pandas.tseries.index.DatetimeIndex'> [1990-10-01 00:00:00, ..., 1990-10-03 18:00:00] Length: 11, Freq: None, Timezone: None
data.columns
Index(['rec1', 'rec2', 'rec3', 'rec4'], dtype='object')
[Inciso] Si no usamos print
, el output del DataFrame
se verá como una tabla HTML gracias a la magia de IPython notebook y de Pandas. En general, intentaremos usar print
como en las anteriores piezas de código y de aquí en adelante para que se vea igual que si se usa algo distinto al notebook. En la pieza de código siguiente no uso print
para qué se vea de lo que hablo ;-)
#data[data.index >= dt.datetime(1990, 10, 2)]
data[data.index >= "1990-10-02"]
rec1 | rec2 | rec3 | rec4 | |
---|---|---|---|---|
fecha_hora | ||||
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 |
7 rows × 4 columns
Escribir el resultado final en un fichero csv, por ejemplo, es algo tan sencillo como:
data.to_csv('dummy.csv')
%load dummy.csv
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.
¿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
.
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'])
df['velocidad']
primero 0.175374 segundo -0.133466 tercero -0.418224 cuarto -0.320517 quinto 0.955521 Name: velocidad, dtype: float64
# Forma alternativa
df.velocidad
primero 0.175374 segundo -0.133466 tercero -0.418224 cuarto -0.320517 quinto 0.955521 Name: velocidad, dtype: float64
Vamos a añadir una columna nueva al DataFrame
:
df['velocidad_maxima'] = np.random.randn(df.shape[0])
print(df)
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)
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]
# 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)
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(u'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(u'Seleccionamos todas las columnas cuyo índice está entre tercero y quinto')
print(u'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(u'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()
print(df['velocidad'][1:3])
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')
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('datos1\n {}'.format(datos1))
print('datos2\n {}'.format(datos2))
print('datos_concatenados\n {}'.format(datos_concatenados_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_concatenados\n {}'.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('datos1\n {}'.format(datos1))
print('datos2\n {}'.format(datos2))
print('datos_concatenados\n {}'.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])
0 -1.691985 1 -1.181241 2 -0.714437 Name: 0, dtype: float64
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
):
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('datos1\n {}'.format(datos1))
print('datos2\n {}'.format(datos2))
print('datos_concatenados\n {}'.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('datos1\n {}'.format(datos1))
print('datos2\n {}'.format(datos2))
print('datos_concatenados\n {}'.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]
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 el apartado 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 recolocados\n{}\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 recolocados\n{}\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]
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.
Además de permitirnos indexar, mover, eliminar,..., datos de formas muy diferentes también podemos usar Pandas para hacer análisis de datos. En este sentido es como un numpy
supervitaminado y supermineralizado (si queréis hacer análisis estádistico más complejo le podéis echar un vistazo a statsmodels, scikit-learn,...
Vamos a usar unos datos que me he descargado de la página del INE. Los he dejado en formato excel para que veamos como también podemos leer datos excel.
nenas = pd.read_excel('nombres_recien_nacidos.xlsx', 'Niñas', header = 1, skiprows = 1)
nenes = pd.read_excel('nombres_recien_nacidos.xlsx', 'Niños', header = 1, skiprows = 1)
nenas.head(5)
nombre | numero | nombre.1 | numero.1 | nombre.2 | numero.2 | nombre.3 | numero.3 | nombre.4 | numero.4 | nombre.5 | numero.5 | nombre.6 | numero.6 | nombre.7 | numero.7 | nombre.8 | numero.8 | nombre.9 | numero.9 | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | LUCIA | 6363 | LUCIA | 6143 | LUCIA | 6624 | LUCIA | 6847 | LUCIA | 8013 | LUCIA | 8192 | LUCIA | 9454 | LUCIA | 10146 | LUCIA | 10370 | LUCIA | 9035 | ... |
1 | MARIA | 5823 | PAULA | 5462 | PAULA | 5859 | PAULA | 6549 | MARIA | 6883 | MARIA | 6927 | MARIA | 7702 | MARIA | 7784 | MARIA | 7865 | MARIA | 8709 | ... |
2 | PAULA | 5375 | MARIA | 5339 | MARIA | 5767 | MARIA | 6113 | PAULA | 6806 | PAULA | 6402 | PAULA | 6516 | PAULA | 6684 | PAULA | 7147 | PAULA | 7003 | ... |
3 | DANIELA | 4748 | SARA | 4345 | DANIELA | 4680 | SARA | 4417 | SARA | 4730 | SARA | 4606 | LAURA | 5144 | LAURA | 5276 | LAURA | 5638 | LAURA | 5451 | ... |
4 | SARA | 4456 | DANIELA | 4270 | SARA | 4662 | DANIELA | 4279 | CARLA | 4271 | LAURA | 4335 | CLAUDIA | 4756 | MARTA | 4592 | MARTA | 4583 | MARTA | 4608 | ... |
5 rows × 22 columns
cols_1 = []
for i in range(2012,2001,-1):
cols_1.append((str(i),'nombre'))
cols_1.append((str(i), 'numero'))
cols = pd.MultiIndex.from_tuples(cols_1, names=['año','campo'])
nenes.columns = cols
nenas.columns = cols
nenes.index = range(1,101)
nenas.index = range(1,101)
nenes.tail(5)
año | 2012 | 2011 | 2010 | 2009 | 2008 | 2007 | 2006 | 2005 | 2004 | 2003 | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
campo | nombre | numero | nombre | numero | nombre | numero | nombre | numero | nombre | numero | nombre | numero | nombre | numero | nombre | numero | nombre | numero | nombre | numero | |
96 | YERAY | 517 | GAEL | 439 | JUANJOSE | 442 | YERAY | 458 | JUANANTONIO | 467 | JUANANTONIO | 465 | BRUNO | 478 | ADAM | 427 | MIKEL | 431 | IZAN | 397 | ... |
97 | GAEL | 501 | YERAY | 439 | JON | 436 | JOSEMARIA | 447 | AIMAR | 464 | CESAR | 461 | SAUL | 477 | JON | 419 | MATEO | 428 | GUILLEM | 385 | ... |
98 | JON | 493 | JON | 431 | NIL | 425 | SERGI | 447 | ROGER | 457 | AIMAR | 458 | JUANCARLOS | 472 | MIKEL | 415 | DARIO | 426 | ABEL | 380 | ... |
99 | MIKEL | 485 | ANDER | 419 | ARTURO | 419 | MIKEL | 444 | JOAQUIN | 446 | ANDER | 443 | ROGER | 469 | FRANCISCOJOSE | 413 | ALFONSO | 418 | MARTI | 376 | ... |
100 | GUILLEM | 479 | JUANJOSE | 418 | JOAQUIN | 419 | KEVIN | 440 | JOSEMARIA | 446 | XAVIER | 442 | FRANCISCOJOSE | 455 | AIMAR | 402 | ANDER | 416 | MATEO | 373 | ... |
5 rows × 22 columns
nenas.T.xs('nombre', level = 'campo')
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
año | |||||||||||||||||||||
2012 | LUCIA | MARIA | PAULA | DANIELA | SARA | CARLA | MARTINA | SOFIA | JULIA | ALBA | VALERIA | CLAUDIA | IRENE | MARTA | LAURA | NOA | CARMEN | ADRIANA | AINHOA | ELENA | ... |
2011 | LUCIA | PAULA | MARIA | SARA | DANIELA | CARLA | SOFIA | ALBA | CLAUDIA | MARTINA | JULIA | MARTA | IRENE | LAURA | VALERIA | CARMEN | ADRIANA | ELENA | NOA | ANA | ... |
2010 | LUCIA | PAULA | MARIA | DANIELA | SARA | CARLA | CLAUDIA | SOFIA | ALBA | IRENE | MARTA | LAURA | JULIA | MARTINA | CARMEN | ADRIANA | AITANA | ELENA | ANA | NOA | ... |
2009 | LUCIA | PAULA | MARIA | SARA | DANIELA | CARLA | CLAUDIA | MARTA | IRENE | SOFIA | ALBA | LAURA | JULIA | CARMEN | MARTINA | ANA | AITANA | ELENA | ANDREA | ADRIANA | ... |
2008 | LUCIA | MARIA | PAULA | SARA | CARLA | CLAUDIA | LAURA | MARTA | IRENE | ALBA | SOFIA | DANIELA | JULIA | ANDREA | ANA | CARMEN | ELENA | NEREA | NATALIA | MARTINA | ... |
2007 | LUCIA | MARIA | PAULA | SARA | LAURA | CLAUDIA | IRENE | MARTA | ALBA | CARLA | ANDREA | JULIA | ANA | SOFIA | NEREA | DANIELA | NATALIA | CARMEN | ELENA | ROCIO | ... |
2006 | LUCIA | MARIA | PAULA | LAURA | CLAUDIA | IRENE | MARTA | ALBA | SARA | CARLA | ANDREA | NEREA | JULIA | NATALIA | ELENA | ANA | SOFIA | CARMEN | DANIELA | MARINA | ... |
2005 | LUCIA | MARIA | PAULA | LAURA | MARTA | ALBA | CLAUDIA | CARLA | ANDREA | SARA | NEREA | IRENE | ANA | NATALIA | JULIA | ELENA | CARMEN | SOFIA | MARINA | CRISTINA | ... |
2004 | LUCIA | MARIA | PAULA | LAURA | MARTA | ALBA | ANDREA | CLAUDIA | SARA | NEREA | CARLA | ANA | NATALIA | IRENE | MARINA | CRISTINA | ELENA | CARMEN | JULIA | AINHOA | ... |
2003 | LUCIA | MARIA | PAULA | LAURA | MARTA | ANDREA | ALBA | SARA | CLAUDIA | ANA | NEREA | CARLA | ELENA | CRISTINA | AINHOA | NATALIA | MARINA | IRENE | CARMEN | NURIA | ... |
2002 | MARIA | LUCIA | PAULA | LAURA | MARTA | ALBA | ANDREA | SARA | ANA | NEREA | CLAUDIA | MARINA | CRISTINA | ELENA | IRENE | NATALIA | CARLA | CARMEN | NURIA | AINHOA | ... |
11 rows × 100 columns
indices = nenas.T.xs('nombre', level = 'campo') == 'LUCIA'
plt.figure(figsize = (20,6))
nenas.T.xs('numero',level = 'campo')[indices].sum().plot(kind='bar')
print(nenas.T.xs('numero',level = 'campo')[indices].sum().sum())
88899.0
plt.figure(figsize = (20,6))
indices = nenas.T.xs('nombre', level = 'campo') == 'LUCIA'
#nenas.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(kind = 'bar', color = 'k')
nenas.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(color = 'k')
print(nenas.T.xs('numero',level = 'campo')[indices].sum().sum())
indices = nenas.T.xs('nombre', level = 'campo') == 'MARIA'
#nenas.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(kind = 'bar', color = 'y', alpha = 0.75)
nenas.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(color = 'y')
print(nenas.T.xs('numero',level = 'campo')[indices].sum().sum())
indices = nenas.T.xs('nombre', level = 'campo') == 'PAULA'
#nenas.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(kind = 'bar', color = 'y', alpha = 0.75)
nenas.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(color = 'b')
print(nenas.T.xs('numero',level = 'campo')[indices].sum().sum())
88899.0 77750.0 69759.0
nenes.T.xs('nombre', level = 'campo')
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
año | |||||||||||||||||||||
2012 | DANIEL | HUGO | ALEJANDRO | PABLO | ALVARO | ADRIAN | DAVID | DIEGO | MARIO | JAVIER | MARCOS | MANUEL | SERGIO | NICOLAS | IKER | LUCAS | IVAN | CARLOS | MIGUEL | JORGE | ... |
2011 | ALEJANDRO | DANIEL | PABLO | HUGO | ALVARO | ADRIAN | DAVID | DIEGO | JAVIER | MARIO | SERGIO | MARCOS | MANUEL | IKER | NICOLAS | JORGE | IVAN | CARLOS | MIGUEL | LUCAS | ... |
2010 | DANIEL | ALEJANDRO | PABLO | HUGO | ALVARO | ADRIAN | DAVID | JAVIER | DIEGO | MARIO | MARCOS | SERGIO | IKER | IVAN | MANUEL | JORGE | AITOR | MIGUEL | CARLOS | SAMUEL | ... |
2009 | DANIEL | ALEJANDRO | PABLO | HUGO | ALVARO | ADRIAN | DAVID | JAVIER | SERGIO | DIEGO | IVAN | MARIO | MARCOS | IKER | MANUEL | CARLOS | JORGE | MIGUEL | SAMUEL | LUCAS | ... |
2008 | DANIEL | ALEJANDRO | PABLO | DAVID | ADRIAN | HUGO | ALVARO | JAVIER | DIEGO | SERGIO | MARCOS | IVAN | IKER | MANUEL | MARIO | JORGE | CARLOS | MIGUEL | RUBEN | ANTONIO | ... |
2007 | DANIEL | ALEJANDRO | PABLO | DAVID | ADRIAN | ALVARO | HUGO | JAVIER | DIEGO | SERGIO | MARCOS | JORGE | IVAN | CARLOS | MANUEL | MARIO | MIGUEL | IKER | RUBEN | ANTONIO | ... |
2006 | ALEJANDRO | DANIEL | PABLO | DAVID | ADRIAN | ALVARO | JAVIER | SERGIO | HUGO | DIEGO | CARLOS | MARCOS | MARIO | IVAN | MANUEL | MIGUEL | JORGE | RUBEN | IKER | RAUL | ... |
2005 | ALEJANDRO | DANIEL | PABLO | DAVID | ADRIAN | JAVIER | ALVARO | SERGIO | CARLOS | MARCOS | IVAN | HUGO | DIEGO | JORGE | MIGUEL | MANUEL | MARIO | RAUL | ANTONIO | RUBEN | ... |
2004 | ALEJANDRO | DAVID | DANIEL | PABLO | ADRIAN | ALVARO | JAVIER | SERGIO | CARLOS | MARCOS | IVAN | HUGO | MARIO | JORGE | DIEGO | MIGUEL | MANUEL | RAUL | RUBEN | ANTONIO | ... |
2003 | ALEJANDRO | DANIEL | PABLO | DAVID | JAVIER | ADRIAN | ALVARO | SERGIO | CARLOS | HUGO | MARIO | JORGE | DIEGO | IVAN | RAUL | MANUEL | MIGUEL | ANTONIO | RUBEN | JUAN | ... |
2002 | ALEJANDRO | PABLO | DANIEL | DAVID | ADRIAN | JAVIER | ALVARO | SERGIO | CARLOS | MARIO | JORGE | RAUL | MANUEL | DIEGO | MIGUEL | IVAN | ANTONIO | JUAN | RUBEN | VICTOR | ... |
11 rows × 100 columns
plt.figure(figsize = (20,6))
indices = nenes.T.xs('nombre', level = 'campo') == 'DANIEL'
nenes.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(color = 'k')
print(nenes.T.xs('numero',level = 'campo')[indices].sum().sum())
indices = nenes.T.xs('nombre', level = 'campo') == 'ALEJANDRO'
nenes.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(color = 'y', alpha = 0.5)
print(nenes.T.xs('numero',level = 'campo')[indices].sum().sum())
indices = nenes.T.xs('nombre', level = 'campo') == 'PABLO'
nenes.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(color = 'b', alpha = 0.5)
print(nenes.T.xs('numero',level = 'campo')[indices].sum().sum())
indices = nenes.T.xs('nombre', level = 'campo') == 'HUGO'
nenes.T.xs('numero',level = 'campo')[indices].sum(axis = 1).plot(color = 'g', alpha = 0.5)
print(nenes.T.xs('numero',level = 'campo')[indices].sum().sum())
68603.0 74439.0 63519.0 44830.0