MÓDULO PANDAS

Ya hemos visto que el módulo NumPy proporciona funciones y rutinas matemáticas para la manipulación de array y matrices de datos numéricos.

La librería pandas de Python proporciona estructuras de datos de alto nivel y herramientas diseñadas específicamente para conseguir un análisis de los datos rápido y sencillo. * pandas está construida sobre la librería NumPy.

Resulta especialmente útil cuando hay que trabajar con:

  • Datos heterogéneos representados de forma tabular
  • Series temporales
  • Matrices
  • Permite el tratamiento de datos perdidos (missing data)
  • Operaciones aritméticas
  • Permite realizar operaciones del álgebra relacional (muy populares en el campo de las bases de datos)

In [1]:
import numpy as np
import pandas as pd

Ya vimos que NumPy ofrecía la estructura de datos ndarray. Del mismo modo, pandas ofrece varias estructuras de datos que nos resultarán de mucha utilidad. Las más utilizadas son Series y DataFrame.


DataFrame

La estructura DataFrame está diseñada para manejar datos representados en forma de tabla. Esta será la estructura de datos más habitual. * Las filas están indexadas. * Las columnas están etiquetadas.

Un DataFrame puede verse como una tabla de SQL o una hoja de cálculo.


CREAR UN DATAFRAME

Hay varias formas de crear un DataFrame. Podemos crearlo indicando los datos que contiene y el nombre de las columnas.


In [5]:
# crear un array de tuplas
nombres = ['Ana','Rafa','Antonio','Pedro','Elena']
nacimientos = [968, 155, 77, 578, 100]

lista_datos = list(zip(nombres, nacimientos))
lista_datos


Out[5]:
[('Ana', 968), ('Rafa', 155), ('Antonio', 77), ('Pedro', 578), ('Elena', 100)]

In [6]:
# crear un DataFrame
tabla = pd.DataFrame( data    = lista_datos,            # lista de tuplas
                      columns = ['Nombres', 'Nacimientos'])  
tabla


Out[6]:
Nombres Nacimientos
0 Ana 968
1 Rafa 155
2 Antonio 77
3 Pedro 578
4 Elena 100

In [7]:
# Añadir una nueva columna
tabla['nueva'] = [7, None, 4, None, None]
tabla


Out[7]:
Nombres Nacimientos nueva
0 Ana 968 7.0
1 Rafa 155 NaN
2 Antonio 77 4.0
3 Pedro 578 NaN
4 Elena 100 NaN

También es posible crear un DataFrame a partir de un diccionario. * Las claves del diccionario serán los nombres de las columnas * Los valores del diccionario constituirán las filas


In [176]:
datos = {"Coffee" : ['Espresso', 'Long Black', 'Flat White', 'Cappuccino', 'Affogato'], 
         "Water"  : [ "No" , "Yes", "No" , "No" , "No" ],
         "Milk"   : ["No","No","Yes",  "Yes - Frothy" ,"No"],
         "Icecream" : ["No", "No","No","No",'Yes'    ]
         }
mi_tabla = pd.DataFrame(datos)
mi_tabla.tail()


Out[176]:
Coffee Icecream Milk Water
0 Espresso No No No
1 Long Black No No Yes
2 Flat White No Yes No
3 Cappuccino No Yes - Frothy No
4 Affogato Yes No No

Podemos crear un DataFrame indexando las filas con un tipo de dato que no sea por defecto (int). Podemos crear un DataFrame cambiando la posición de las columnas, incluso con menos columnas.


In [177]:
datos = {"Coffee" : ['Espresso', 'Long Black', 'Flat White', 'Cappuccino', 'Affogato'], 
              "Water"  : [ "No" , "Yes", "No" , "No" , "No" ],
              "Milk"   : ["No","No","Yes",  "Yes - Frothy" ,"No"],
              "Icecream" : ["No", "No","No","No",'Yes'    ]
         }
mi_tabla = pd.DataFrame(datos, 
                        columns = ['Coffee',  'Water', 'Milk'], 
                        index   = [2001, 2003, 2002, 2005, 2010] )
mi_tabla


Out[177]:
Coffee Water Milk
2001 Espresso No No
2003 Long Black Yes No
2002 Flat White No Yes
2005 Cappuccino No Yes - Frothy
2010 Affogato No No

Y si alguna columna no existe, el sistema pone NaN (not a number) para indicar que el valor no existe.


In [178]:
mi_tabla = pd.DataFrame(datos, columns = ['Coffee',  'Water', 'Milk', 'Price'], 
                               index  =[2001, 2003, 2002, 2005, 2010])
mi_tabla


Out[178]:
Coffee Water Milk Price
2001 Espresso No No NaN
2003 Long Black Yes No NaN
2002 Flat White No Yes NaN
2005 Cappuccino No Yes - Frothy NaN
2010 Affogato No No NaN

Para conocer el nombre de las columnas de un DataFrame usamos el atributo columns, mientras que para conocer el nombre de las filas usamos el atributo index.


In [179]:
mi_tabla.columns


Out[179]:
Index(['Coffee', 'Water', 'Milk', 'Price'], dtype='object')

In [180]:
mi_tabla.index


Out[180]:
Int64Index([2001, 2003, 2002, 2005, 2010], dtype='int64')

Podemos realizar algunas preguntas sobre los índices:


In [181]:
2005 in mi_tabla.index


Out[181]:
True

In [182]:
mi_tabla.values


Out[182]:
array([['Espresso', 'No', 'No', nan],
       ['Long Black', 'Yes', 'No', nan],
       ['Flat White', 'No', 'Yes', nan],
       ['Cappuccino', 'No', 'Yes - Frothy', nan],
       ['Affogato', 'No', 'No', nan]], dtype=object)

In [183]:
type(mi_tabla.values)


Out[183]:
numpy.ndarray

Series

Un objeto del tipo Series es como un DataFrame pero con solo una columna. Se trata de un objto de tipo array de una dimensión que tiene asociado un array de índices.


In [184]:
s = pd.Series([10, 20, 30,40])
s


Out[184]:
0    10
1    20
2    30
3    40
dtype: int64

In [185]:
precio = pd.Series([10, 20, 30,40], 
         index=['Espresso', 'Long Black', 'Flat White', 'Cappuccino'])                         
precio


Out[185]:
Espresso      10
Long Black    20
Flat White    30
Cappuccino    40
dtype: int64

In [186]:
type(precio)


Out[186]:
pandas.core.series.Series


ACCESO A LOS DATOS DE UN DATAFRAME

Para acceder a las columnas, podemos usar la notación '.' (como si fuera un atributo) o con la notación empleada en los diccionarios, utilizando como clave el nombre de la columna.


In [187]:
mi_tabla


Out[187]:
Coffee Water Milk Price
2001 Espresso No No NaN
2003 Long Black Yes No NaN
2002 Flat White No Yes NaN
2005 Cappuccino No Yes - Frothy NaN
2010 Affogato No No NaN

In [188]:
# Acceso a la columna
mi_tabla.Coffee


Out[188]:
2001      Espresso
2003    Long Black
2002    Flat White
2005    Cappuccino
2010      Affogato
Name: Coffee, dtype: object

In [189]:
# También se puede acceder asi
mi_tabla [ 'Coffee' ]


Out[189]:
2001      Espresso
2003    Long Black
2002    Flat White
2005    Cappuccino
2010      Affogato
Name: Coffee, dtype: object

Para acceder a partes de un DataFrame, conjunto de filas, etc., se puede utilizar la misma notación empleada para los arrays de Numpy, pero esa opción es poco eficiente en el caso de DataFrames.

No hay que olvidar que estamos estudiando las herramientas que tiene Python para tratar de forma eficiente con grandes volúmenes de datos. La alternativa es utilizar las funciones at, iat, loc, iloc.

  • ix: similar a loc e iloc.

In [190]:
# Selección de las 3 primeras filas
# Notación típica en NumPy - Poco eficiente
mi_tabla[0:3]


Out[190]:
Coffee Water Milk Price
2001 Espresso No No NaN
2003 Long Black Yes No NaN
2002 Flat White No Yes NaN

Selección por etiqueta - loc


In [191]:
# Selección por etiqueta
# OJO: Ambos se incluyen
mi_tabla.loc[2001:2002]


Out[191]:
Coffee Water Milk Price
2001 Espresso No No NaN
2003 Long Black Yes No NaN
2002 Flat White No Yes NaN

In [192]:
# Selección por etiqueta
mi_tabla.loc[2001:2002, ['Coffee', 'Milk']]


Out[192]:
Coffee Milk
2001 Espresso No
2003 Long Black No
2002 Flat White Yes

In [193]:
# Selección por etiqueta - resultado un escalar
mi_tabla.loc[2001, 'Coffee']


Out[193]:
'Espresso'

In [194]:
# Selección por etiqueta - resultado un escalar
mi_tabla.at[2001, 'Coffee']                      # más eficiente


Out[194]:
'Espresso'

Selección por posición - iloc

Para acceder a las filas, se accede por índice mediante la función iloc:


In [195]:
# Acceso a la primera fila del DataFrame 
# El resultado es una serie
mi_tabla.iloc[1]


Out[195]:
Coffee    Long Black
Water            Yes
Milk              No
Price            NaN
Name: 2003, dtype: object

In [196]:
# Acceso a las filas con índices 1 y 2, 
# Columnas desde aquella con índice 1 hasta el final.
mi_tabla.iloc[1:3,1:]


Out[196]:
Water Milk Price
2003 Yes No NaN
2002 No Yes NaN

In [197]:
print(mi_tabla)
# Acesso al valor de una casilla
mi_tabla.iloc[1,0]


          Coffee Water          Milk Price
2001    Espresso    No            No   NaN
2003  Long Black   Yes            No   NaN
2002  Flat White    No           Yes   NaN
2005  Cappuccino    No  Yes - Frothy   NaN
2010    Affogato    No            No   NaN
Out[197]:
'Long Black'

In [198]:
# Acesso al valor de una casilla (igual que el anterior)
mi_tabla.iat[1,0]    # más rápida que la anterior


Out[198]:
'Long Black'

ACTUALIZACIÓN DE LOS DATOS DE DATAFRAME

Para actualizar los datos de una columna podemos utilizar un valor concreto o un array de valores.


In [199]:
mi_tabla.Price = .85
mi_tabla


Out[199]:
Coffee Water Milk Price
2001 Espresso No No 0.85
2003 Long Black Yes No 0.85
2002 Flat White No Yes 0.85
2005 Cappuccino No Yes - Frothy 0.85
2010 Affogato No No 0.85

In [200]:
mi_tabla.Price = [.85, 0.95, 0.80, 1.10, 0.65]
mi_tabla


Out[200]:
Coffee Water Milk Price
2001 Espresso No No 0.85
2003 Long Black Yes No 0.95
2002 Flat White No Yes 0.80
2005 Cappuccino No Yes - Frothy 1.10
2010 Affogato No No 0.65

Podemos calcular el precio medio del café.


In [201]:
medio_cafe = mi_tabla.Price.mean()
medio_cafe


Out[201]:
0.8699999999999999

In [202]:
pp = mi_tabla.mean()
pp


Out[202]:
Price    0.87
dtype: float64

In [203]:
mi_tabla


Out[203]:
Coffee Water Milk Price
2001 Espresso No No 0.85
2003 Long Black Yes No 0.95
2002 Flat White No Yes 0.80
2005 Cappuccino No Yes - Frothy 1.10
2010 Affogato No No 0.65

Las operaciones sobre DataFrames son vectorizadas, al igual que ocurría con las operaciones sobre los objetos ndarray de NumPy.

Podemos aumentar el precio del café 10 céntimos.


In [204]:
mi_tabla['nuevo_pvp'] = mi_tabla.Price  + 0.10
mi_tabla


Out[204]:
Coffee Water Milk Price nuevo_pvp
2001 Espresso No No 0.85 0.95
2003 Long Black Yes No 0.95 1.05
2002 Flat White No Yes 0.80 0.90
2005 Cappuccino No Yes - Frothy 1.10 1.20
2010 Affogato No No 0.65 0.75


BORRAR COLUMNAS DE UN DATAFRAME (POP Y DROP)

La función pop borra la columna especificada del DataFrame y guarda la columna borrada en un objeto de tipo Serie.

  • Modifica el DataFrame

La función drop permite borrar tanto filas como columnas.

  • No modifica el DataFrame. El resultado lo guarda en una copia.
  • No permite acceder a los elementos borrados.

In [205]:
# Borra la columna 'Price'
columna_price = mi_tabla.pop('Price')
columna_price


Out[205]:
2001    0.85
2003    0.95
2002    0.80
2005    1.10
2010    0.65
Name: Price, dtype: float64

In [206]:
type(columna_price)


Out[206]:
pandas.core.series.Series

In [207]:
mi_tabla


Out[207]:
Coffee Water Milk nuevo_pvp
2001 Espresso No No 0.95
2003 Long Black Yes No 1.05
2002 Flat White No Yes 0.90
2005 Cappuccino No Yes - Frothy 1.20
2010 Affogato No No 0.75

También es posible utilizar la función drop para eliminar tanto filas como columnas.


In [208]:
# Borramos las filas correspondientes a los índices 2005 y 2010
mi_tabla.drop([2005,2010])
mi_tabla


Out[208]:
Coffee Water Milk nuevo_pvp
2001 Espresso No No 0.95
2003 Long Black Yes No 1.05
2002 Flat White No Yes 0.90
2005 Cappuccino No Yes - Frothy 1.20
2010 Affogato No No 0.75

In [209]:
# Borramos las columnas Water y Milk
# Hay que especificar el eje que contiene las etiquetas
otro = mi_tabla.drop(['Water', 'Milk'], 1)
otro


Out[209]:
Coffee nuevo_pvp
2001 Espresso 0.95
2003 Long Black 1.05
2002 Flat White 0.90
2005 Cappuccino 1.20
2010 Affogato 0.75


INSERTAR DATOS EN UN DATAFRAME

Ya hemos podido ver cómo se pueden insertar nuevas columnas a un DataFrame de forma muy sencilla:


In [210]:
# Nueva columna
mi_tabla['Chocolat'] = ['No', 'No', np.nan, 'Yes', 'No' ]
mi_tabla


Out[210]:
Coffee Water Milk nuevo_pvp Chocolat
2001 Espresso No No 0.95 No
2003 Long Black Yes No 1.05 No
2002 Flat White No Yes 0.90 NaN
2005 Cappuccino No Yes - Frothy 1.20 Yes
2010 Affogato No No 0.75 No

También podemos utilizar la función insert para insertar la columna Price que hemos eliminado anteriormente en una determinada posición.


In [211]:
# Inserta la columna en la segunda posición 
mi_tabla.insert(2, 'Price', columna_price)
mi_tabla


Out[211]:
Coffee Water Price Milk nuevo_pvp Chocolat
2001 Espresso No 0.85 No 0.95 No
2003 Long Black Yes 0.95 No 1.05 No
2002 Flat White No 0.80 Yes 0.90 NaN
2005 Cappuccino No 1.10 Yes - Frothy 1.20 Yes
2010 Affogato No 0.65 No 0.75 No


COMBINAR DATAFRAMES

append, merge, join

Pandas provee varias formas de combinar Series y DataFrames.

Las filas de un DataFrame se pueden insertar mediante la función append especificando un diccionario, donde la clave es el nombre de la columna.

  • Podemos insertar una fila parcial: Las columnas no especificadas pasan a tomar el valor NaN.
  • Peligro: se pierde el nombre de los índices, que pasan a ser enteros.

In [212]:
# Nueva fila (Total o parcial)
# -- se pierde el valor de los índices !!!!!!
# Crea un indice autonumerado
mi_tabla.append({ 'Coffee': 'Other',
                  'Water': 'No',
                  'Chocolat': 'Yes',
                  'Price' : 0.50 },
                ignore_index = True)


Out[212]:
Coffee Water Price Milk nuevo_pvp Chocolat
0 Espresso No 0.85 No 0.95 No
1 Long Black Yes 0.95 No 1.05 No
2 Flat White No 0.80 Yes 0.90 NaN
3 Cappuccino No 1.10 Yes - Frothy 1.20 Yes
4 Affogato No 0.65 No 0.75 No
5 Other No 0.50 NaN NaN Yes

Para no perder el nombre de los índices, se realiza la operación append con un DataFrame en lugar de un diccionario:


In [213]:
# Creamos el data frame a insertar
otro_df = pd.DataFrame({  'Coffee': 'Other',
                          'Water': 'No',
                          'Chocolat': 'Yes',
                          'Price' : 0.50 },
                       index = [2015])

# Insertamos el data frame mediante la función 'append'
mi_tabla.append(otro_df)


Out[213]:
Chocolat Coffee Milk Price Water nuevo_pvp
2001 No Espresso No 0.85 No 0.95
2003 No Long Black No 0.95 Yes 1.05
2002 NaN Flat White Yes 0.80 No 0.90
2005 Yes Cappuccino Yes - Frothy 1.10 No 1.20
2010 No Affogato No 0.65 No 0.75
2015 Yes Other NaN 0.50 No NaN

In [214]:
mi_tabla


Out[214]:
Coffee Water Price Milk nuevo_pvp Chocolat
2001 Espresso No 0.85 No 0.95 No
2003 Long Black Yes 0.95 No 1.05 No
2002 Flat White No 0.80 Yes 0.90 NaN
2005 Cappuccino No 1.10 Yes - Frothy 1.20 Yes
2010 Affogato No 0.65 No 0.75 No

Cuidado!, la función append no modifica el objeto.


In [215]:
mi_tabla = mi_tabla.append(otro_df )
mi_tabla


Out[215]:
Chocolat Coffee Milk Price Water nuevo_pvp
2001 No Espresso No 0.85 No 0.95
2003 No Long Black No 0.95 Yes 1.05
2002 NaN Flat White Yes 0.80 No 0.90
2005 Yes Cappuccino Yes - Frothy 1.10 No 1.20
2010 No Affogato No 0.65 No 0.75
2015 Yes Other NaN 0.50 No NaN

MERGE: La operación join al estilo del álgebra relacional

La función merge permite combinar filas de dos o más tablas (DataFrames) basándose en una o varias claves. Trabaja de forma muy eficiente, de forma similar a las bases de datos relacionales tipo SQL. Hay tres tipos de merge:

  • interno (inner)
  • externo (outer)
  • cruzado(left, right)

ENTORNO

inner join: Cada fila de la tabla df1 se combina con otro de la tabla df2 que cumpla las condiciones (igualdad de una o varias columnas).


In [216]:
# inner join on key
df1 = pd.DataFrame({'key': ['K0', 'K1', 'K1', 'K3'],  
                  'A': ['A0', 'A1', 'A2', 'A3'],    
                  'B': ['B0', 'B1', 'B2', 'B3']})   
                                                    
                                                    
df2 = pd.DataFrame({'key': ['K0', 'K1', 'K2', 'K3'], 
                   'A': ['C0', 'C1', 'C2', 'C3'],   
                   'D': ['D0', 'D1', 'D2', 'D3']})  
                                                    
                                                    
result = pd.merge(df1, df2, on='key')               
print (df1)
print (df2)
result


    A   B key
0  A0  B0  K0
1  A1  B1  K1
2  A2  B2  K1
3  A3  B3  K3
    A   D key
0  C0  D0  K0
1  C1  D1  K1
2  C2  D2  K2
3  C3  D3  K3
Out[216]:
A_x B key A_y D
0 A0 B0 K0 C0 D0
1 A1 B1 K1 C1 D1
2 A2 B2 K1 C1 D1
3 A3 B3 K3 C3 D3

In [217]:
# inner join on (key1 key2)
df1 = pd.DataFrame({'key1': ['K0', 'K0', 'K1', 'K2'],       
                     'key2': ['K0', 'K1', 'K0', 'K1'],       
                     'A': ['A0', 'A1', 'A2', 'A3'],          
                     'B': ['B0', 'B1', 'B2', 'B3']})         
                                                          
                                                          
df2 = pd.DataFrame({'key1': ['K0', 'K1', 'K1', 'K2'],      
                      'key2': ['K0', 'K0', 'K0', 'K0'],      
                      'C': ['C0', 'C1', 'C2', 'C3'],         
                      'D': ['D0', 'D1', 'D2', 'D3']})        
                                                          
                                                          
result = pd.merge( df1, df2, 
                              on=['key1', 'key2'])  

print (df1)
print (df2)
result


    A   B key1 key2
0  A0  B0   K0   K0
1  A1  B1   K0   K1
2  A2  B2   K1   K0
3  A3  B3   K2   K1
    C   D key1 key2
0  C0  D0   K0   K0
1  C1  D1   K1   K0
2  C2  D2   K1   K0
3  C3  D3   K2   K0
Out[217]:
A B key1 key2 C D
0 A0 B0 K0 K0 C0 D0
1 A2 B2 K1 K0 C1 D1
2 A2 B2 K1 K0 C2 D2

left join: todas las filas de la tabla izquierda df1 tengan o no correspondencia con la tabla derecha. Las que no tengan correspondencia se rellenan con NaN.


In [218]:
result = pd.merge(df1, df2, 
                              how='left', 
                              on=['key1', 'key2'])

print (df1)
print (df2)
result


    A   B key1 key2
0  A0  B0   K0   K0
1  A1  B1   K0   K1
2  A2  B2   K1   K0
3  A3  B3   K2   K1
    C   D key1 key2
0  C0  D0   K0   K0
1  C1  D1   K1   K0
2  C2  D2   K1   K0
3  C3  D3   K2   K0
Out[218]:
A B key1 key2 C D
0 A0 B0 K0 K0 C0 D0
1 A1 B1 K0 K1 NaN NaN
2 A2 B2 K1 K0 C1 D1
3 A2 B2 K1 K0 C2 D2
4 A3 B3 K2 K1 NaN NaN

outer join: Es la unión de left outer join y right outer join


In [219]:
result = pd.merge(df1, df2, 
                              how='outer', 
                              on=['key1', 'key2'])
result


Out[219]:
A B key1 key2 C D
0 A0 B0 K0 K0 C0 D0
1 A1 B1 K0 K1 NaN NaN
2 A2 B2 K1 K0 C1 D1
3 A2 B2 K1 K0 C2 D2
4 A3 B3 K2 K1 NaN NaN
5 NaN NaN K2 K0 C3 D3

JOIN : Combinación de tablas por índices

La función join permite combinar dos tablas de dos formas:

  • índices sobre índices: mantiene la información de los índices.
  • columnas sobre índices.

Es posible realizar **inner_join**mediante**how = inner**, left_join mediante how = left, right_join mediante how = right.


In [220]:
otro_df = pd.DataFrame([3,2,5,3,2], 
                                columns = ['Stars',],
                                index = [2001, 2002, 2005, 2015, 2016])
print(otro_df)
mi_tabla.join(otro_df)


      Stars
2001      3
2002      2
2005      5
2015      3
2016      2
Out[220]:
Chocolat Coffee Milk Price Water nuevo_pvp Stars
2001 No Espresso No 0.85 No 0.95 3.0
2003 No Long Black No 0.95 Yes 1.05 NaN
2002 NaN Flat White Yes 0.80 No 0.90 2.0
2005 Yes Cappuccino Yes - Frothy 1.10 No 1.20 5.0
2010 No Affogato No 0.65 No 0.75 NaN
2015 Yes Other NaN 0.50 No NaN 3.0

En el ejemplo anterior se han perdido los datos referentes al índice 2016. Para conservarlos es necesario indicar cómo queremos que se realice la operación join. En este caso usaremos how = outer para que se conserven todos los índices.


In [221]:
otro_df = pd.DataFrame(  [3,2,5,3,2], 
                                columns = ['Stars',],
                                index = [2001, 2002, 2005, 2015, 2016])
mi_tabla.join(otro_df, how = 'outer')


Out[221]:
Chocolat Coffee Milk Price Water nuevo_pvp Stars
2001 No Espresso No 0.85 No 0.95 3.0
2002 NaN Flat White Yes 0.80 No 0.90 2.0
2003 No Long Black No 0.95 Yes 1.05 NaN
2005 Yes Cappuccino Yes - Frothy 1.10 No 1.20 5.0
2010 No Affogato No 0.65 No 0.75 NaN
2015 Yes Other NaN 0.50 No NaN 3.0
2016 NaN NaN NaN NaN NaN NaN 2.0

Ahora veamos un ejemplo de combinación de columnas sobre índices:


In [222]:
otro_df = pd.DataFrame([[1,2],[3,4]], 
                                columns = ['Val1','Val2'],
                                index = ['Cappuccino', 'Espresso'])
otro_df


Out[222]:
Val1 Val2
Cappuccino 1 2
Espresso 3 4

In [223]:
# Los valores de la columna Coffee hacen la función de índices del dataframe
mi_tabla.join(otro_df, on = 'Coffee', how = 'outer')


Out[223]:
Chocolat Coffee Milk Price Water nuevo_pvp Val1 Val2
2001 No Espresso No 0.85 No 0.95 3.0 4.0
2003 No Long Black No 0.95 Yes 1.05 NaN NaN
2002 NaN Flat White Yes 0.80 No 0.90 NaN NaN
2005 Yes Cappuccino Yes - Frothy 1.10 No 1.20 1.0 2.0
2010 No Affogato No 0.65 No 0.75 NaN NaN
2015 Yes Other NaN 0.50 No NaN NaN NaN

In [ ]: