EDA

¿Qué es?

La EDA (Exploratory Data Anlayisis) nos permite analizar los datos que tenemos en un database de manera intuitiva y grafíca asi de poder mejor comprender sus peculiaridades y llegar a una mejor comprensión así de facilitar las fases sucesivas.

La definición formal es:

En estadística, el análisis exploratorio de datos es un enfoque para analizar conjuntos de datos para resumir sus principales características, a menudo con métodos visuales. Se puede usar o no un modelo estadístico, pero principalmente EDA es para ver qué nos pueden decir los datos más allá del modelado formal o la tarea de prueba de hipótesis.

Podemos por lo tanto tener unos insight que nos serán utilzes luego tanto para preparar los datos para las faces sucesivas (removiendo las irregularidades) y tener una visión aproximativa de los mismos.

Basandonos en este anlísis podemos empezar a toamr algunas decisiones en la estructuración de los datos y en la definición de los modelos a utilizar:

  • si la hacemos bien podemos ahorrarnos mucho trabajo

  • si la hacemos mal arriesgamos a perder información valiosa

Temos 5 fases principales en el EDA:

  1. Recompilación y carga de datos

  2. Limpieza datos

  3. Analísis univariada

  4. Analísis bivariada

  5. Analísis multivariada

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

Recompilación y carga de datos

Ya vimos en varios laboratorios como podemos cargar los datos de diferentes archivos, con apis e introducirlos manualmente. Cuidados con las fuentes de los datos y como tenemos que citarlos y como lo podemos compartir.

Subimos por lo tantos los datos que pueden encontrar aquí para replicas.

db=pd.read_csv("Marketing_Analysis.csv",low_memory=False)
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
<ipython-input-2-469d18f944a3> in <module>
----> 1 db=pd.read_csv("Marketing_Analysis.csv",low_memory=False)

~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/parsers.py in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, squeeze, prefix, mangle_dupe_cols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, dialect, error_bad_lines, warn_bad_lines, delim_whitespace, low_memory, memory_map, float_precision)
    684     )
    685 
--> 686     return _read(filepath_or_buffer, kwds)
    687 
    688 

~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/parsers.py in _read(filepath_or_buffer, kwds)
    450 
    451     # Create the parser.
--> 452     parser = TextFileReader(fp_or_buf, **kwds)
    453 
    454     if chunksize or iterator:

~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/parsers.py in __init__(self, f, engine, **kwds)
    944             self.options["has_index_names"] = kwds["has_index_names"]
    945 
--> 946         self._make_engine(self.engine)
    947 
    948     def close(self):

~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/parsers.py in _make_engine(self, engine)
   1176     def _make_engine(self, engine="c"):
   1177         if engine == "c":
-> 1178             self._engine = CParserWrapper(self.f, **self.options)
   1179         else:
   1180             if engine == "python":

~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/parsers.py in __init__(self, src, **kwds)
   2006         kwds["usecols"] = self.usecols
   2007 
-> 2008         self._reader = parsers.TextReader(src, **kwds)
   2009         self.unnamed_cols = self._reader.unnamed_cols
   2010 

pandas/_libs/parsers.pyx in pandas._libs.parsers.TextReader.__cinit__()

pandas/_libs/parsers.pyx in pandas._libs.parsers.TextReader._setup_parser_source()

FileNotFoundError: [Errno 2] No such file or directory: 'Marketing_Analysis.csv'
db.shape
(45211, 19)
db.head(5)
customerid age salary balance marital jobedu targeted default housing loan contact day month duration campaign pdays previous poutcome response
0 1 58.0 100000 2143 married management,tertiary yes no yes no unknown 5 may, 2017 261 sec 1 -1 0 unknown no
1 2 44.0 60000 29 single technician,secondary yes no yes no unknown 5 may, 2017 151 sec 1 -1 0 unknown no
2 3 33.0 120000 2 married entrepreneur,secondary yes no yes yes unknown 5 may, 2017 76 sec 1 -1 0 unknown no
3 4 47.0 20000 1506 married blue-collar,unknown no no yes no unknown 5 may, 2017 92 sec 1 -1 0 unknown no
4 5 33.0 0 1 single unknown,unknown no no no no unknown 5 may, 2017 198 sec 1 -1 0 unknown no
db.set_index("customerid", inplace=  True)
db.head(3)
age salary balance marital jobedu targeted default housing loan contact day month duration campaign pdays previous poutcome response
customerid
1 58.0 100000 2143 married management,tertiary yes no yes no unknown 5 may, 2017 261 sec 1 -1 0 unknown no
2 44.0 60000 29 single technician,secondary yes no yes no unknown 5 may, 2017 151 sec 1 -1 0 unknown no
3 33.0 120000 2 married entrepreneur,secondary yes no yes yes unknown 5 may, 2017 76 sec 1 -1 0 unknown no
db.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 45211 entries, 1 to 45211
Data columns (total 18 columns):
 #   Column    Non-Null Count  Dtype  
---  ------    --------------  -----  
 0   age       45191 non-null  float64
 1   salary    45211 non-null  int64  
 2   balance   45211 non-null  int64  
 3   marital   45211 non-null  object 
 4   jobedu    45211 non-null  object 
 5   targeted  45211 non-null  object 
 6   default   45211 non-null  object 
 7   housing   45211 non-null  object 
 8   loan      45211 non-null  object 
 9   contact   45211 non-null  object 
 10  day       45211 non-null  int64  
 11  month     45161 non-null  object 
 12  duration  45211 non-null  object 
 13  campaign  45211 non-null  int64  
 14  pdays     45211 non-null  int64  
 15  previous  45211 non-null  int64  
 16  poutcome  45211 non-null  object 
 17  response  45181 non-null  object 
dtypes: float64(1), int64(6), object(11)
memory usage: 6.6+ MB
db.columns
Index(['age', 'salary', 'balance', 'marital', 'jobedu', 'targeted', 'default',
       'housing', 'loan', 'contact', 'day', 'month', 'duration', 'campaign',
       'pdays', 'previous', 'poutcome', 'response'],
      dtype='object')

Ahora si nada más checkamos valores null:

db.isnull().sum()
age         20
salary       0
balance      0
marital      0
jobedu       0
targeted     0
default      0
housing      0
loan         0
contact      0
day          0
month       50
duration     0
campaign     0
pdays        0
previous     0
poutcome     0
response    30
dtype: int64

2. Limpieza datos

Ahora procedemos a la limpieza del D:

  1. Eliminamos eventuales columnas/renglones de resume

  2. Unimos eventualmetne columna para entender mejor los datos

  3. de igual forma dividir eventaules otras columnas

  4. Averiguamos que todos las columnas tengan nombre y sobre todo el indice

En nuestro caso tenemos:

db.head(5)
age salary balance marital jobedu targeted default housing loan contact day month duration campaign pdays previous poutcome response
customerid
1 58.0 100000 2143 married management,tertiary yes no yes no unknown 5 may, 2017 261 sec 1 -1 0 unknown no
2 44.0 60000 29 single technician,secondary yes no yes no unknown 5 may, 2017 151 sec 1 -1 0 unknown no
3 33.0 120000 2 married entrepreneur,secondary yes no yes yes unknown 5 may, 2017 76 sec 1 -1 0 unknown no
4 47.0 20000 1506 married blue-collar,unknown no no yes no unknown 5 may, 2017 92 sec 1 -1 0 unknown no
5 33.0 0 1 single unknown,unknown no no no no unknown 5 may, 2017 198 sec 1 -1 0 unknown no

¿Qué notamos?

db['job']= db["jobedu"].apply(lambda x: x.split(",")[0])
db['education']= db["jobedu"].apply(lambda x: x.split(",")[1])
db
age salary balance marital jobedu targeted default housing loan contact day month duration campaign pdays previous poutcome response job education
customerid
1 58.0 100000 2143 married management,tertiary yes no yes no unknown 5 may, 2017 261 sec 1 -1 0 unknown no management tertiary
2 44.0 60000 29 single technician,secondary yes no yes no unknown 5 may, 2017 151 sec 1 -1 0 unknown no technician secondary
3 33.0 120000 2 married entrepreneur,secondary yes no yes yes unknown 5 may, 2017 76 sec 1 -1 0 unknown no entrepreneur secondary
4 47.0 20000 1506 married blue-collar,unknown no no yes no unknown 5 may, 2017 92 sec 1 -1 0 unknown no blue-collar unknown
5 33.0 0 1 single unknown,unknown no no no no unknown 5 may, 2017 198 sec 1 -1 0 unknown no unknown unknown
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
45207 51.0 60000 825 married technician,tertiary yes no no no cellular 17 nov, 2017 16.2833333333333 min 3 -1 0 unknown yes technician tertiary
45208 71.0 55000 1729 divorced retired,primary yes no no no cellular 17 nov, 2017 7.6 min 2 -1 0 unknown yes retired primary
45209 72.0 55000 5715 married retired,secondary yes no no no cellular 17 nov, 2017 18.7833333333333 min 5 184 3 success yes retired secondary
45210 57.0 20000 668 married blue-collar,secondary yes no no no telephone 17 nov, 2017 8.46666666666667 min 4 -1 0 unknown no blue-collar secondary
45211 37.0 120000 2971 married entrepreneur,secondary yes no no no cellular 17 nov, 2017 6.01666666666667 min 2 188 11 other no entrepreneur secondary

45211 rows × 20 columns

db.drop(labels='jobedu', axis = 1, inplace = True)
db
age salary balance marital targeted default housing loan contact day month duration campaign pdays previous poutcome response job education
customerid
1 58.0 100000 2143 married yes no yes no unknown 5 may, 2017 261 sec 1 -1 0 unknown no management tertiary
2 44.0 60000 29 single yes no yes no unknown 5 may, 2017 151 sec 1 -1 0 unknown no technician secondary
3 33.0 120000 2 married yes no yes yes unknown 5 may, 2017 76 sec 1 -1 0 unknown no entrepreneur secondary
4 47.0 20000 1506 married no no yes no unknown 5 may, 2017 92 sec 1 -1 0 unknown no blue-collar unknown
5 33.0 0 1 single no no no no unknown 5 may, 2017 198 sec 1 -1 0 unknown no unknown unknown
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
45207 51.0 60000 825 married yes no no no cellular 17 nov, 2017 16.2833333333333 min 3 -1 0 unknown yes technician tertiary
45208 71.0 55000 1729 divorced yes no no no cellular 17 nov, 2017 7.6 min 2 -1 0 unknown yes retired primary
45209 72.0 55000 5715 married yes no no no cellular 17 nov, 2017 18.7833333333333 min 5 184 3 success yes retired secondary
45210 57.0 20000 668 married yes no no no telephone 17 nov, 2017 8.46666666666667 min 4 -1 0 unknown no blue-collar secondary
45211 37.0 120000 2971 married yes no no no cellular 17 nov, 2017 6.01666666666667 min 2 188 11 other no entrepreneur secondary

45211 rows × 19 columns

Ahora manejamos los valores faltantes, podemos tener de tres tipos:

  1. MCAR(Missing completely at random): no dependen de alguna caracteristica en particular

  2. MAR(Missing at random): pueden depender de alguna caracteristica

  3. MNAR(Missing not at random): hay alguna razón para faltar

Droppamos todos lo que no tienen edad definida:

db = db[~db.age.isnull()].copy()
db.isnull().sum()
db.shape
(45161, 20)

Ahora imputamos los valores para los meses:

month_mode = db.month.mode()[0]
month_mode
'may, 2017'

Ahora procedemos a substituir este valor en nuestra columna con faltante:

db.month.fillna(month_mode, inplace = True)
db.month.isnull().sum()
0

Ahora manejamos los faltantes en nuestra variable dependiente (response):

db = db[~db.response.isnull()].copy()
db.isnull().sum()
age          0
salary       0
balance      0
marital      0
targeted     0
default      0
housing      0
loan         0
contact      0
day          0
month        0
duration     0
campaign     0
pdays        0
previous     0
poutcome     0
response     0
job          0
education    0
dtype: int64

Podriamos sobstituir nuestro valores faltantes con NaN para que no haya afectaciones cuando procedemos al analisís estadistico

Outliers

Son unos datos que se discostan remarcablemente del siguiente punto más cercano. Podemos tener:

  1. Outlier univariado

  2. Outlier multivariado/bivariado: cuando ploteamos una variable con otra el valor se discosta del valor esperado de manera anormal

Antes de proceder a eso tenemos que estandardizar los valores para que escalas diferentes no nos lleven a conclusiones equivocadas. Tenemos diferentes tipologias de datos que podemos encontrar

Analisis univariado de variable categorica no ordenada

  • no tienen peso o medida

  • no tienen orden

db.job.value_counts()
blue-collar      9722
management       9451
technician       7589
admin.           5165
services         4148
retired          2262
self-employed    1574
entrepreneur     1484
unemployed       1302
housemaid        1238
student           938
unknown           288
Name: job, dtype: int64
db.job.value_counts(normalize=True)
blue-collar      0.215274
management       0.209273
technician       0.168043
admin.           0.114369
services         0.091849
retired          0.050087
self-employed    0.034853
entrepreneur     0.032860
unemployed       0.028830
housemaid        0.027413
student          0.020770
unknown          0.006377
Name: job, dtype: float64
Podemo plotear nuestro analisis:
db.job.value_counts(normalize=True).plot.barh()
plt.show()
../../_images/EDA_30_0.png

Analisis univariada Variable categorica ordenada

Estas variables tienen un orden natural, por ejemplo:

  • Meses

  • Educación

db.education.value_counts(normalize=True)
db.education.value_counts().plot.pie()
plt.show()
../../_images/EDA_33_0.png

Si tenemos una variable con valores numericos podemos calcular

db.salary.describe()
count     45161.000000
mean      57004.849317
std       32087.698810
min           0.000000
25%       20000.000000
50%       60000.000000
75%       70000.000000
max      120000.000000
Name: salary, dtype: float64

Finalmente podemos plotear una densidad de la distribución de la varaible numerica:

sns.distplot(db['salary'])
C:\ProgramData\Anaconda3\lib\site-packages\seaborn\distributions.py:2557: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms).
  warnings.warn(msg, FutureWarning)
<AxesSubplot:xlabel='salary', ylabel='Density'>
../../_images/EDA_37_2.png
sns.displot(db['salary'])
<seaborn.axisgrid.FacetGrid at 0x15f1d76c348>
../../_images/EDA_38_1.png
sns.histplot(db['balance'])
plt.xlim(-8019, 102127)
plt.ylim(0,3000)
(0.0, 3000.0)
../../_images/EDA_39_1.png
db.balance.describe()
count     45161.000000
mean       1362.850690
std        3045.939589
min       -8019.000000
25%          72.000000
50%         448.000000
75%        1428.000000
max      102127.000000
Name: balance, dtype: float64

Analisis bivariada

Cuando hacemos un analisis bivariada el uso de graficas nos puede ser extremadamente utíl. En particular los más ocupados son:

  • scatter plot

  • pair plot

  • matriz de correlación

Scatter plot

plt.scatter(db.salary,db.balance)
plt.show()

db.plot.scatter(x="age",y="balance")
plt.show()
../../_images/EDA_42_0.png ../../_images/EDA_42_1.png

Pair plot

sns.pairplot(data = db, vars=['salary','balance','age'])
plt.show()
../../_images/EDA_44_0.png

QQ-Plot

Es un método gráfico para el diagnóstico de diferencias entre la distribución de probabilidad de una población de la que se ha extraído una muestra aleatoria y una distribución usada para la comparación.

import scipy.stats as stats
iris = sns.load_dataset('iris')


rvs = stats.norm(loc=0, scale=1)
normal_sample = rvs.rvs(size=100000)
sns.set()
sns.distplot(normal_sample)
C:\ProgramData\Anaconda3\lib\site-packages\seaborn\distributions.py:2557: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms).
  warnings.warn(msg, FutureWarning)
<AxesSubplot:ylabel='Density'>
../../_images/EDA_46_2.png
normal_sample = rvs.rvs(size=10000)
stats.probplot(normal_sample, dist="norm", plot=plt)
plt.show()
../../_images/EDA_47_0.png
sns.distplot(stats.expon().rvs(size=10000))
C:\ProgramData\Anaconda3\lib\site-packages\seaborn\distributions.py:2557: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms).
  warnings.warn(msg, FutureWarning)
<AxesSubplot:ylabel='Density'>
../../_images/EDA_48_2.png
expon_rvs = stats.expon().rvs(size=100000)
normal_rvs = stats.norm().rvs(size=100000)
stats.probplot(x=expon_rvs, dist=stats.norm(), plot=plt)
((array([-4.34602155, -4.14724578, -4.03913009, ...,  4.03913009,
          4.14724578,  4.34602155]),
  array([2.16825700e-06, 1.57624189e-05, 2.93375619e-05, ...,
         1.16652885e+01, 1.21702078e+01, 1.34591407e+01])),
 (0.8996537488205563, 0.9969476608219366, 0.9033984542371438))
../../_images/EDA_49_1.png
expon_rvs = stats.expon().rvs(size=100000)
stats.probplot(x=expon_rvs, dist="expon", plot=plt)
plt.show()
../../_images/EDA_50_0.png

Matrices de correlación

Cuando queremos ver en un unico grafico las relaciones entre más de una variable de forma sintetica

db[['age','salary','balance']].corr()
age salary balance
age 1.000000 0.024513 0.097710
salary 0.024513 1.000000 0.055489
balance 0.097710 0.055489 1.000000
sns.heatmap(db[['age','salary','balance']].corr(), annot=True, cmap = 'Blues')
plt.show()
../../_images/EDA_53_0.png

variables categoricas numericas

Normalmente se analizan con base en su media, mediana y un instrumento particularmente util es el boxplot.

Calculamolos a manita:

db.groupby('response')['salary'].mean()
response
no     56769.510482
yes    58780.510880
Name: salary, dtype: float64
db.groupby('response')['salary'].median()
response
no     60000
yes    60000
Name: salary, dtype: int64

Pareciera no haber grandes diferencias, pero analizando los boxplot descubrimos que:

sns.boxplot(x=db.response, y=db.salary)
plt.show()
../../_images/EDA_58_0.png
ax = sns.boxplot(x='response', y='salary', data=db)
plt.setp(ax.artists, alpha=.5, linewidth=2, edgecolor="k")
plt.xticks(rotation=45)
(array([0, 1]), [Text(0, 0, 'no'), Text(1, 0, 'yes')])
../../_images/EDA_59_1.png

Analisis variables categoricas

Queremos analizar como diferentes varbles categoricas (como la educación) tienen impacto en la variable dependiente.. Para hacer eso necesitamos convertir Response en una variable binaria.

db['response_rate'] = np.where(db.response=='yes',1,0)
db.response_rate.value_counts()
0    39876
1     5285
Name: response_rate, dtype: int64
Analizando ahora como se distribuye frente distintos valores de la una variable categorica tendremos:
db.groupby('marital')['response_rate'].mean().plot.bar()
plt.show()
../../_images/EDA_63_0.png

Analisis multivariado

Antes tenemos crear una tabla pivot con en las columnas los diferentes status civiles:

result = pd.pivot_table(data=db, index='education', columns='marital',values='response_rate')
result
marital divorced married single
education
primary 0.138852 0.075601 0.106808
secondary 0.103559 0.094650 0.129271
tertiary 0.137415 0.129835 0.183737
unknown 0.142012 0.122519 0.162879

Ahora como antes vamos a imprimir una mapa de calor.

sns.heatmap(result, annot=True, cmap = 'RdYlGn', center=0.117)
plt.show()
../../_images/EDA_67_0.png

Apis y carca de datos

Podemos cargar los datos de local, commo hicimos antes en nuestro ejercicio, por medio de una url o con unas APIs.

#metodo URL

url = "http://climatedataapi.worldbank.org/climateweb/rest/v1/country/cru/tas/year/ROU.csv"
df = pd.read_csv(url)

df

Pero ahora sí pasamos a las APis

Blockquote Una API, o interfaz de programación de aplicaciones, es un servidor que puede utilizar para recuperar y enviar datos mediante código. Las API se utilizan con mayor frecuencia para recuperar datos.

¿Por que usar una API?

  1. Los datos cambía rápido, y no tiene sentido estar generando un dataset y bajandolo cada munuto.

  2. Necesitas acceder a sólo una parte de los datos

  3. Hay una computación repetida involucrada

Podemos hacer diferentes acciones con las API:

  • GET

  • POST

  • PUT

  • DELETE

import requests
response = requests.get('https://google.com/')
print(response)
<Response [200]>

La libreria request tiene algunas funciones especificas:

print(response.status_code)
200
if response:
  print('Request is successful.')
else:
  print('Request returned an error.')
Request is successful.

El endpoint es una dirección, usuaklmente, hacía donde apunta nuestro pedido. Normalmente el nombre de esta dirección es autodescriptivo.

response = requests.get("http://api.open-notify.org/astros.json")
print(response.status_code)
200
pd.DataFrame(response.json()["people"])
name craft
0 Mark Vande Hei ISS
1 Oleg Novitskiy ISS
2 Pyotr Dubrov ISS
3 Thomas Pesquet ISS
4 Megan McArthur ISS
5 Shane Kimbrough ISS
6 Akihiko Hoshide ISS

Muchas veces necesitamos una llave para poder acceder a los datos que requerimos. Igual hay un verdadero mercado y sitio especializados para esto.

Por medio de los encabezaados HTTPS se manejan algunas informaciones sobre las APIs

response = requests.get("https://api.thedogapi.com/v1/breeds/1")
response.headers
{'Content-Encoding': 'gzip', 'Content-Type': 'application/json; charset=utf-8', 'Date': 'Fri, 07 May 2021 12:19:43 GMT', 'Server': 'Apache/2.4.46 (Amazon)', 'Strict-Transport-Security': 'max-age=15552000; includeSubDomains', 'Vary': 'Origin,Accept-Encoding', 'X-Content-Type-Options': 'nosniff', 'X-DNS-Prefetch-Control': 'off', 'X-Download-Options': 'noopen', 'X-Frame-Options': 'SAMEORIGIN', 'X-Response-Time': '2ms', 'X-XSS-Protection': '1; mode=block', 'Content-Length': '265', 'Connection': 'keep-alive'}

Ya sabiendo que es un json, podemos acceder a el atravez de:

response.json()["name"]
'Affenpinscher'
endpoint = "https://www.googleapis.com/books/v1/volumes"
query = "la divina comedia"

params = {"q": query, "maxResults": 3}
response = requests.get(endpoint, params=params).json()
for book in response["items"]:
    volume = book["volumeInfo"]
    title = volume["title"]
    published = volume["authors"]
    print(f"{title} {published} ")
La Divina Comedia ['Dante Alighieri'] 
La divina comedia ['Dante Alghieri'] 
La Divina Comedia por Dante Alighieri ['Dante Alighieri'] 
response
{'kind': 'books#volumes',
 'totalItems': 734,
 'items': [{'kind': 'books#volume',
   'id': 'WrPCDwAAQBAJ',
   'etag': 'j6ynahmNPaE',
   'selfLink': 'https://www.googleapis.com/books/v1/volumes/WrPCDwAAQBAJ',
   'volumeInfo': {'title': 'La Divina Comedia',
    'authors': ['Dante Alighieri'],
    'publisher': 'Good Press',
    'publishedDate': '2019-11-11',
    'description': '"La Divina Comedia" de Dante Alighieri (traducido por Manuel Aranda y Sanjuan) de la Editorial Good Press. Good Press publica una gran variedad de títulos que abarca todos los géneros. Van desde los títulos clásicos famosos, novelas, textos documentales y crónicas de la vida real, hasta temas ignorados o por ser descubiertos de la literatura universal. Editorial Good Press divulga libros que son una lectura imprescindible. Cada publicación de Good Press ha sido corregida y formateada al detalle, para elevar en gran medida su facilidad de lectura en todos los equipos y programas de lectura electrónica. Nuestra meta es la producción de Libros electrónicos que sean versátiles y accesibles para el lector y para todos, en un formato digital de alta calidad.',
    'industryIdentifiers': [{'type': 'OTHER',
      'identifier': 'EAN:4057664122674'}],
    'readingModes': {'text': True, 'image': True},
    'pageCount': 941,
    'printType': 'BOOK',
    'categories': ['Fiction'],
    'maturityRating': 'NOT_MATURE',
    'allowAnonLogging': True,
    'contentVersion': '1.5.5.0.preview.3',
    'panelizationSummary': {'containsEpubBubbles': False,
     'containsImageBubbles': False},
    'imageLinks': {'smallThumbnail': 'http://books.google.com/books/content?id=WrPCDwAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api',
     'thumbnail': 'http://books.google.com/books/content?id=WrPCDwAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api'},
    'language': 'es',
    'previewLink': 'http://books.google.com.mx/books?id=WrPCDwAAQBAJ&pg=PP1&dq=la+divina+comedia&hl=&cd=1&source=gbs_api',
    'infoLink': 'https://play.google.com/store/books/details?id=WrPCDwAAQBAJ&source=gbs_api',
    'canonicalVolumeLink': 'https://play.google.com/store/books/details?id=WrPCDwAAQBAJ'},
   'saleInfo': {'country': 'MX',
    'saleability': 'FOR_SALE',
    'isEbook': True,
    'listPrice': {'amount': 25, 'currencyCode': 'MXN'},
    'retailPrice': {'amount': 25, 'currencyCode': 'MXN'},
    'buyLink': 'https://play.google.com/store/books/details?id=WrPCDwAAQBAJ&rdid=book-WrPCDwAAQBAJ&rdot=1&source=gbs_api',
    'offers': [{'finskyOfferType': 1,
      'listPrice': {'amountInMicros': 25000000, 'currencyCode': 'MXN'},
      'retailPrice': {'amountInMicros': 25000000, 'currencyCode': 'MXN'},
      'giftable': True}]},
   'accessInfo': {'country': 'MX',
    'viewability': 'PARTIAL',
    'embeddable': True,
    'publicDomain': False,
    'textToSpeechPermission': 'ALLOWED',
    'epub': {'isAvailable': True},
    'pdf': {'isAvailable': True},
    'webReaderLink': 'http://play.google.com/books/reader?id=WrPCDwAAQBAJ&hl=&printsec=frontcover&source=gbs_api',
    'accessViewStatus': 'SAMPLE',
    'quoteSharingAllowed': False},
   'searchInfo': {'textSnippet': '&quot;La Divina Comedia&quot; de Dante Alighieri (traducido por Manuel Aranda y Sanjuan) de la Editorial Good Press.'}},
  {'kind': 'books#volume',
   'id': 'wGPSDgAAQBAJ',
   'etag': 'NOQVX9XaqiA',
   'selfLink': 'https://www.googleapis.com/books/v1/volumes/wGPSDgAAQBAJ',
   'volumeInfo': {'title': 'La divina comedia',
    'subtitle': '',
    'authors': ['Dante Alghieri'],
    'publisher': 'NoBooks Editorial',
    'publishedDate': '1976',
    'readingModes': {'text': True, 'image': True},
    'pageCount': 441,
    'printType': 'BOOK',
    'maturityRating': 'NOT_MATURE',
    'allowAnonLogging': True,
    'contentVersion': 'preview-1.0.0',
    'panelizationSummary': {'containsEpubBubbles': False,
     'containsImageBubbles': False},
    'imageLinks': {'smallThumbnail': 'http://books.google.com/books/content?id=wGPSDgAAQBAJ&printsec=frontcover&img=1&zoom=5&edge=curl&source=gbs_api',
     'thumbnail': 'http://books.google.com/books/content?id=wGPSDgAAQBAJ&printsec=frontcover&img=1&zoom=1&edge=curl&source=gbs_api'},
    'language': 'es',
    'previewLink': 'http://books.google.com.mx/books?id=wGPSDgAAQBAJ&printsec=frontcover&dq=la+divina+comedia&hl=&cd=2&source=gbs_api',
    'infoLink': 'https://play.google.com/store/books/details?id=wGPSDgAAQBAJ&source=gbs_api',
    'canonicalVolumeLink': 'https://play.google.com/store/books/details?id=wGPSDgAAQBAJ'},
   'saleInfo': {'country': 'MX',
    'saleability': 'FOR_SALE',
    'isEbook': True,
    'listPrice': {'amount': 28.84, 'currencyCode': 'MXN'},
    'retailPrice': {'amount': 28.84, 'currencyCode': 'MXN'},
    'buyLink': 'https://play.google.com/store/books/details?id=wGPSDgAAQBAJ&rdid=book-wGPSDgAAQBAJ&rdot=1&source=gbs_api',
    'offers': [{'finskyOfferType': 1,
      'listPrice': {'amountInMicros': 28840000, 'currencyCode': 'MXN'},
      'retailPrice': {'amountInMicros': 28840000, 'currencyCode': 'MXN'},
      'giftable': True}]},
   'accessInfo': {'country': 'MX',
    'viewability': 'PARTIAL',
    'embeddable': True,
    'publicDomain': False,
    'textToSpeechPermission': 'ALLOWED',
    'epub': {'isAvailable': True},
    'pdf': {'isAvailable': True},
    'webReaderLink': 'http://play.google.com/books/reader?id=wGPSDgAAQBAJ&hl=&printsec=frontcover&source=gbs_api',
    'accessViewStatus': 'SAMPLE',
    'quoteSharingAllowed': False}},
  {'kind': 'books#volume',
   'id': 'O0ESIQu9_7wC',
   'etag': 'bzi0xAGR2sk',
   'selfLink': 'https://www.googleapis.com/books/v1/volumes/O0ESIQu9_7wC',
   'volumeInfo': {'title': 'La Divina Comedia por Dante Alighieri',
    'subtitle': 'El Infierno',
    'authors': ['Dante Alighieri'],
    'publishedDate': '1870',
    'industryIdentifiers': [{'type': 'OTHER',
      'identifier': 'IBNF:CF005685501'}],
    'readingModes': {'text': False, 'image': False},
    'pageCount': 232,
    'printType': 'BOOK',
    'maturityRating': 'NOT_MATURE',
    'allowAnonLogging': False,
    'contentVersion': '0.2.1.0.preview.0',
    'panelizationSummary': {'containsEpubBubbles': False,
     'containsImageBubbles': False},
    'imageLinks': {'smallThumbnail': 'http://books.google.com/books/content?id=O0ESIQu9_7wC&printsec=frontcover&img=1&zoom=5&source=gbs_api',
     'thumbnail': 'http://books.google.com/books/content?id=O0ESIQu9_7wC&printsec=frontcover&img=1&zoom=1&source=gbs_api'},
    'language': 'es',
    'previewLink': 'http://books.google.com.mx/books?id=O0ESIQu9_7wC&q=la+divina+comedia&dq=la+divina+comedia&hl=&cd=3&source=gbs_api',
    'infoLink': 'http://books.google.com.mx/books?id=O0ESIQu9_7wC&dq=la+divina+comedia&hl=&source=gbs_api',
    'canonicalVolumeLink': 'https://books.google.com/books/about/La_Divina_Comedia_por_Dante_Alighieri.html?hl=&id=O0ESIQu9_7wC'},
   'saleInfo': {'country': 'MX',
    'saleability': 'NOT_FOR_SALE',
    'isEbook': False},
   'accessInfo': {'country': 'MX',
    'viewability': 'NO_PAGES',
    'embeddable': False,
    'publicDomain': False,
    'textToSpeechPermission': 'ALLOWED',
    'epub': {'isAvailable': False},
    'pdf': {'isAvailable': False},
    'webReaderLink': 'http://play.google.com/books/reader?id=O0ESIQu9_7wC&hl=&printsec=frontcover&source=gbs_api',
    'accessViewStatus': 'NONE',
    'quoteSharingAllowed': False}}]}

Fuentes: