Análisis Exploratorio de Datos

Hemos hablado mucho de datos, todo muy de libro o juguete. Esta clase intentará acercarte a algunos de los principales desafíos a la hora de trabajar con distintas fuentes de datos y los problemas usuales que podrías encontrar.

Fuentes de datos

Para variar un poco, utilizaremos la librería pathlib en lugar de os para manejar directorios. El paradigma es un poco distinto, en lugar de muchas funciones, la filosofía es tratar a los directorios como objetos que tienen sus propios métodos y operaciones.

import numpy as np
import pandas as pd
from pathlib import Path
data_path = Path().resolve().parent / "data"
print(data_path)
/home/runner/work/mat281_2020S2/mat281_2020S2/data

CSV

Del inglés Comma-Separated Values, los archivos CSV utilizan comas (“,”) para separar valores y cada registro consiste de una fila.

  • Pros:

    • Livianos.

    • De fácil entendimiento.

    • Editables usando un editor de texto.

  • Contras:

    • No está totalmente estandarizado (e.g. ¿Qué pasa si un valor tiene comas?)

    • Son sensible al encoding (es la forma en que se codifica un carácter).

Pandas posee su propia función para leer csv: pd.read_csv().

# Documentación
# pd.read_csv?

Un ejemplo de encoding incorrecto

pd.read_csv(data_path / "encoding_example.csv", sep=",", encoding="gbk")
nombre apellido edad
0 Juan P茅rez 12.0
1 Le贸n Pardo 29.0
2 Jos茅 Nu帽ez NaN

Mientras que el mismo dataset con el encoding correcto luce así

pd.read_csv(data_path / "encoding_example.csv", sep=",", encoding="utf-8")
nombre apellido edad
0 Juan Pérez 12.0
1 León Pardo 29.0
2 José Nuñez NaN

JSON

Acrónimo de JavaScript Object Notation, utilizado principalmente para intercambiar datos entre una aplicación web y un servidor.

  • Pros:

    • Livianos.

    • De fácil entendimiento.

    • Editables usando un editor de texto.

    • Formato estandarizado.

  • Contras:

    • La lectura con pandas puede ser un poco complicada.

Pandas posee su propia función para leer JSON: pd.read_json().

# pd.read_json?

Se parecen mucho a los diccionarios de python pero en un archivo de texto.

!head ../data/json_example.json
{
  "integer": {
    "0": 5,
    "1": 5,
    "2": 9,
    "3": 6,
    "4": 6,
    "5": 9,
    "6": 7,
    "7": 1,
pd.read_json(data_path / "json_example.json", orient="columns").head()
integer datetime category
0 5 2015-01-01 00:00:00 0
1 5 2015-01-01 00:00:01 0
2 9 2015-01-01 00:00:02 0
3 6 2015-01-01 00:00:03 0
4 6 2015-01-01 00:00:04 0

Pickle

Es un módulo que implementa protocolos binarios de serialización y des-serialización de objetos de Python.

  • Pros

    • Puede representar una inmensa cantidad de tipos de objetos de python.

    • En un contexto de seguridad, como no es legible por el ser humano (representación binaria) puede ser útil para almacenar datos sensibles.

  • Contras:

    • Solo Python.

    • Si viene de un tercero podría tener contenido malicioso.

Pandas posee su propia función para leer pickles: pd.read_pickle().

# pd.read_pickle?
pd.read_pickle(data_path / 'nba.pkl').head()
name year_start year_end position height weight birth_date college
0 Alaa Abdelnaby 1991 1995 F-C 6-10 240.0 June 24, 1968 Duke University
1 Zaid Abdul-Aziz 1969 1978 C-F 6-9 235.0 April 7, 1946 Iowa State University
2 Kareem Abdul-Jabbar 1970 1989 C 7-2 225.0 April 16, 1947 University of California, Los Angeles
3 Mahmoud Abdul-Rauf 1991 2001 G 6-1 162.0 March 9, 1969 Louisiana State University
4 Tariq Abdul-Wahad 1998 2003 F 6-6 223.0 November 3, 1974 San Jose State University

SQL

Conocimos las bases de datos relacionales SQL en clases anteriores y como recordarás existe la función pd.read_sql(), lo interesante aquí es que debes crear una conexión antes de poder leer la base de datos. Cada Sistema de Gestión de Bases de Datos Relacionales (Relational Database Management System o RDBMS) tiene su propia forma de conectarse.

# pd.read_sql?
import sqlite3
connector = sqlite3.connect(data_path / "chinook.db")
pd.read_sql_query("select * from albums", con=connector).head()
AlbumId Title ArtistId
0 1 For Those About To Rock We Salute You 1
1 2 Balls to the Wall 2
2 3 Restless and Wild 2
3 4 Let There Be Rock 1
4 5 Big Ones 3

API

¿Has escuchado el término API? Fuera de todo tecnicismo, las APIs (Application Programming Interface) permiten hacer uso de funciones ya existentes en otro software (o de la infraestructura ya existente en otras plataformas) para no estar reinventando la rueda constantemente, reutilizando así código que se sabe que está probado y que funciona correctamente. Por ejemplo, cuando haces una compra online y utilizas WebPay o una página utiliza los mapas de GoogleMaps. ¡Hay APIs en todos lados!

Utilizaremos la API de Open Notify para obtener cuántas personas hay en el espacio en este momento (link).

import requests
response = requests.get("http://api.open-notify.org/astros.json")
print(f"response has type {type(response)}")
print(response)
response has type <class 'requests.models.Response'>
<Response [200]>

Puedes acceder a su contenido como un JSON de la siguiente manera

response.json()
{'message': 'success',
 'number': 7,
 'people': [{'craft': 'ISS', 'name': 'Sergey Ryzhikov'},
  {'craft': 'ISS', 'name': 'Kate Rubins'},
  {'craft': 'ISS', 'name': 'Sergey Kud-Sverchkov'},
  {'craft': 'ISS', 'name': 'Mike Hopkins'},
  {'craft': 'ISS', 'name': 'Victor Glover'},
  {'craft': 'ISS', 'name': 'Shannon Walker'},
  {'craft': 'ISS', 'name': 'Soichi Noguchi'}]}

Lo cual en la práctica lo carga como un diccionario en Python

type(response.json())
dict

Por lo que podemos cargar ciertas estructuras a dataframes con métodos de pandas que utilicen diccionarios. Por dar un ejemplo, dentro del JSON obtenido hay una lista de personas.

pd.DataFrame.from_dict(response.json()["people"])
craft name
0 ISS Sergey Ryzhikov
1 ISS Kate Rubins
2 ISS Sergey Kud-Sverchkov
3 ISS Mike Hopkins
4 ISS Victor Glover
5 ISS Shannon Walker
6 ISS Soichi Noguchi

Manos a la obra

El análisis exploratorio de datos es una forma de analizar datos definido por John W. Tukey (E.D.A.: Exploratory data analysis) es el tratamiento estadístico al que se someten las muestras recogidas durante un proceso de investigación en cualquier campo científico. Para mayor rapidez y precisión, todo el proceso suele realizarse por medios informáticos, con aplicaciones específicas para el tratamiento estadístico.

El análisis exploratorio de datos debería dar respuestas (al menos) a lo siguiente:

  1. ¿Qué pregunta(s) estás tratando de resolver (o probar que estás equivocado)?

  2. ¿Qué tipo de datos tiene y cómo trata los diferentes tipos?

  3. ¿Qué falta en los datos y cómo los maneja?

  4. ¿Qué hacer con los datos faltantes, outliers o información mal inputada?

  5. ¿Se puede sacar más provecho a los datos ?

Ejemplo: Datos de terremotos

El dataset earthquakes.csv contiene la información de los terremotos de los países durante el año 2000 al 2011. Debido a que la información de este dataset es relativamente fácil de trabajar, hemos creado un dataset denominado earthquakes_contaminated.csv que posee información contaminada en cada una de sus columnas. De esta forma se podrá ilustrar los distintos inconvenientes al realizar análisis exploratorio de datos.

pd.read_csv(data_path / "earthquakes.csv").head()
Año Pais Magnitud
0 2011 Turkey 7.1
1 2011 India 6.9
2 2011 Japan 7.1
3 2011 Burma 6.8
4 2011 Japan 9.0
earthquakes = pd.read_csv(data_path / "earthquakes_contaminated.csv")
earthquakes.head()
Año Pais Magnitud Informacion
0 2000 Turkey 6 info no valiosa
1 2000 Turkmenistan 7 info no valiosa
2 2000 Azerbaijan 6.5 info no valiosa
3 2000 Azerbaijan 6.8 info no valiosa
4 2000 Papua New Guinea 8 info no valiosa

Variables

  • Pais:

    • Descripción: País del devento sísmico.

    • Tipo: string

    • Observaciones: No deberían encontrarse nombres de ciudades, comunas, pueblos, estados, etc.

  • Año:

    • Descripción: Año del devento sísmico.

    • Tipo: integer

    • Observaciones: Los años deben estar entre 2000 y 2011.

  • Magnitud:

  • Informacion:

    • Descripción: Columna contaminante.

    • Tipo: string

    • Observaciones: A priori pareciera que no entrega información a los datos.

A pesar que la magnitud es un float, el conocimiento de los datos nos da información relevante, pues el terremoto con mayor magnitud registrado a la fecha fue el de Valdivia, Chile el 22 de mayo de 1960 con una magnitud entre 9.4 - 9.6.

Los datos son solo bytes en el disco duro si es que no entregan valor y conocimiento.

¿Qué pregunta(s) estás tratando de resolver (o probar que estás equivocado)?

A modo de ejemplo, consideremos que que queremos conocer la mayor magnitud de terremoto en cada país a lo largo de los años.

¿Qué tipo de datos tiene y cómo trata los diferentes tipos?

Por el conocimiento de los datos sabemos que Pais e Información son variables categóricas, mientras que Año y Magnitud son variables numéricas.

Utilizemos las herramientas que nos entrega pandas.

earthquakes.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 228 entries, 0 to 227
Data columns (total 4 columns):
 #   Column       Non-Null Count  Dtype 
---  ------       --------------  ----- 
 0   Año          226 non-null    object
 1   Pais         226 non-null    object
 2   Magnitud     225 non-null    object
 3   Informacion  220 non-null    object
dtypes: object(4)
memory usage: 7.2+ KB
earthquakes.describe(include="all").T
count unique top freq
Año 226 16 2003 31
Pais 226 74 Indonesia 27
Magnitud 225 45 6.4 14
Informacion 220 3 info valiosa 166
earthquakes.dtypes
Año            object
Pais           object
Magnitud       object
Informacion    object
dtype: object

Todas las columnas son de tipo object, sospechoso. Además, algunas no tienen datos.

Tip: Típicamente se utilizan nombres de columnas en minúsculas y sin espacios. Un truco es hacer lo siguiente:

earthquakes = earthquakes.rename(columns=lambda x: x.lower().strip())
earthquakes.head()
año pais magnitud informacion
0 2000 Turkey 6 info no valiosa
1 2000 Turkmenistan 7 info no valiosa
2 2000 Azerbaijan 6.5 info no valiosa
3 2000 Azerbaijan 6.8 info no valiosa
4 2000 Papua New Guinea 8 info no valiosa

Se le aplicó una función lambda a cada nombre de columna! Puum!

¿Qué falta en los datos y cómo los maneja?

No es necesario agregar más variables, pero si procesarla.

¿Qué hacer con los datos faltantes, outliers o información mal inputada?

A continuación iremos explorando cada una de las columnas.

for col in earthquakes:
    print(f"La columna {col} posee los siguientes valores únicos:\n {earthquakes[col].sort_values().unique()}\n\n")
La columna año posee los siguientes valores únicos:
 ['1990' '1997' '1999' '2000' '2001' '2002' '2003' '2004' '2005' '2006'
 '2007' '2008' '2009' '2010' '2011' 'dos mil uno' nan]


La columna pais posee los siguientes valores únicos:
 ['Afghanistan' 'Afghanistan ' 'Algeria' 'Algeria ' 'Argentina'
 'Azerbaijan' 'Azerbaijan ' 'Bangladesh' 'Burma ' 'Chile' 'Chile ' 'China'
 'China ' 'Colombia' 'Costa Rica' 'Costa Rica '
 'Democratic Republic of the Congo' 'Democratic Republic of the Congo '
 'Dominican Republic' 'Ecuador' 'El Salvador ' 'Greece' 'Greece '
 'Guadeloupe' 'Guatemala' 'Haiti ' 'India' 'India ' 'Indonesia'
 'Indonesia ' 'Iran' 'Iran ' 'Iran, 2005 Qeshm earthquake' 'Italy'
 'Italy ' 'Japan' 'Japan ' 'Kazakhstan' 'Kyrgyzstan ' 'Martinique'
 'Mexico ' 'Morocco' 'Morocco ' 'Mozambique' 'New Zealand' 'New Zealand '
 'Nicaragua' 'Pakistan' 'Pakistan ' 'Panama' 'Papua New Guinea' 'Peru'
 'Peru ' 'Philippines' 'Russian Federation' 'Rwanda' 'Samoa ' 'Serbia'
 'Slovenia' 'Solomon Islands ' 'Taiwan' 'Taiwan ' 'Tajikistan'
 'Tajikistan ' 'Tanzania' 'Tanzania ' 'Turkey' 'Turkey ' 'Turkmenistan'
 'United States ' 'Venezuela' 'Vietnam' 'arica' 'shile' nan]


La columna magnitud posee los siguientes valores únicos:
 ['-10' '2002-Tanzania-5.8' '2003-japan-8.5' '4.7' '4.9' '5' '5.1' '5.2'
 '5.3' '5.4' '5.5' '5.6' '5.7' '5.8' '5.9' '6' '6.1' '6.2' '6.3' '6.4'
 '6.5' '6.6' '6.7' '6.8' '6.9' '7' '7.1' '7.2' '7.3' '7.4' '7.5' '7.6'
 '7.7' '7.8' '7.9' '8' '8.1' '8.3' '8.4' '8.5' '8.6' '8.8' '9' '9.1' '9.7'
 nan]


La columna informacion posee los siguientes valores únicos:
 ['info no valiosa' 'info valiosa' 'valiosa' nan]
  • En la columna año se presentan las siguientes anomalías:

    • Datos vacíos.

    • Años sin importancia: Se ha establecido que los años de estudios son desde el año 2000 al 2011.

    • Nombres mal escritos: en este caso sabemos que ‘dos mil uno’ corresponde a ‘2001’.

  • En la columna pais se presentan las siguientes anomalías:

    • Datos vacíos.

    • Ciudades, e.g. arica.

    • Países mal escritos e.g. shile.

    • Países repetidos pero mal formateados, e.g. Turkey.

    • Cruce de información, e.g. Iran, 2005 Qeshm earthquake.

  • En la columna magnitud se presentan las siguientes anomalías:

    • Datos vacíos.

    • Cruce de información, e.g. 2002-Tanzania-5.8.

    • Valores imposibles, e.g. 9.7.

  • La columna informacion realmente no está entregando ninguna información valiosa al problema.

Partamos por eliminar la columna informacion.

eqk = earthquakes.drop(columns="informacion")  # A veces es importante no sobrescribir el dataframe original para realizar análisis posteriores.
eqk.head()
año pais magnitud
0 2000 Turkey 6
1 2000 Turkmenistan 7
2 2000 Azerbaijan 6.5
3 2000 Azerbaijan 6.8
4 2000 Papua New Guinea 8

Respecto a la columna año, corregir estos errores no es difícil, pero suele ser tedioso. Aparte que si no se realiza un correcto análisis es posible no detectar estos errores a tiempo. Empecemos con los registros nulos.

eqk.loc[lambda x: x["año"].isnull()]
año pais magnitud
225 NaN NaN 2002-Tanzania-5.8
226 NaN NaN 2003-japan-8.5

Veamos el archivo

! sed  -n "226,228p" data/earthquakes_contaminated.csv
sed: can't read data/earthquakes_contaminated.csv: No such file or directory

Toda la información está contenida en una columna!

Para editar la información usaremos dos herramientas:

  • Los métodos de str en pandas, en particular para dividir una columna.

  • loc para asignar los nuevos valores.

eqk.loc[lambda x: x["año"].isnull(), "magnitud"].str.split("-", expand=True).values
array([['2002', 'Tanzania', '5.8'],
       ['2003', 'japan', '8.5']], dtype=object)
eqk.loc[lambda x: x["año"].isnull(), :] = eqk.loc[lambda x: x["año"].isnull(), "magnitud"].str.split("-", expand=True).values
eqk.loc[[225, 226]]
año pais magnitud
225 2002 Tanzania 5.8
226 2003 japan 8.5

Ahora los registros que no se pueden convertir a numeric. Veamos que no es posible convertirlo.

try:
    eqk["año"].astype(np.int)
except Exception as e:
    print(e)
invalid literal for int() with base 10: 'dos mil uno'
eqk["año"].str.isnumeric().fillna(False)
0      True
1      True
2      True
3      True
4      True
       ... 
223    True
224    True
225    True
226    True
227    True
Name: año, Length: 228, dtype: bool
eqk.loc[lambda x: ~ x["año"].str.isnumeric()]
año pais magnitud
31 dos mil uno China 5.4

Veamos el valor a cambiar

eqk.loc[lambda x: ~ x["año"].str.isnumeric(), "año"].iloc[0]
'dos mil uno'

Reemplazar es muy fácil!

eqk["año"].str.replace("dos mil uno", "2001")
0      2000
1      2000
2      2000
3      2000
4      2000
       ... 
223    1990
224    1999
225    2002
226    2003
227    2005
Name: año, Length: 228, dtype: object

Para asignar en el dataframe basta con:

eqk["año"] = eqk["año"].str.replace("dos mil uno", "2001").astype(np.int)

La forma encadenada sería:

# eqk["año"] = eqk.assign(año=lambda x: x["año"].str.replace("dos mil uno", "2001").astype("int"))
eqk.dtypes
año          int64
pais        object
magnitud    object
dtype: object

Finalmentem, filtremos los años necesarios:

eqk = eqk.query("2000 <= año <= 2011")

Siguiendo de forma análoga con la columna magnitud.

eqk.loc[lambda x: x["magnitud"].isnull()]
año pais magnitud
219 2010 Colombia NaN
220 2005 Indonesia NaN
221 2010 Venezuela NaN

La verdad es que no hay mucho que hacer con estos valores, por el momento no inputaremos ningún valor y los descartaremos.

eqk = eqk.loc[lambda x: x["magnitud"].notnull()]
try:
    eqk["magnitud"].astype(np.float)
    print("Ya es posible transformar la columna a float.")
except:
    print("Aún no es posible transformar la columna a float.")
Ya es posible transformar la columna a float.
eqk = eqk.astype({"magnitud": np.float})
eqk.dtypes
año           int64
pais         object
magnitud    float64
dtype: object
eqk.magnitud.unique()
array([  6. ,   7. ,   6.5,   6.8,   8. ,   5.7,   6.4,   5.5,   6.3,
         5.4,   6.1,   6.7,   7.9,   7.2,   7.5,   5.3,   5.9,   9.7,
         5.8,   4.7,   7.6,   8.4,   5. ,   5.6,   6.6,   6.2,   7.1,
         7.3,   5.1,   5.2,   8.3,   6.9,   9.1,   4.9,   7.8,   8.6,
         7.7,   7.4,   8.5,   8.1,   8.8,   9. , -10. ])
eqk.query("magnitud < 0 or 9.6 < magnitud")
año pais magnitud
22 2000 shile 9.7
217 2011 shile -10.0
218 2011 shile -10.0
eqk = eqk.query("0 <= magnitud <= 9.6")
eqk.query("magnitud < 0 or 9.6 < magnitud")
año pais magnitud

Finalmente, para la columna pais. Comenzaremos con los nombres erróneos, estos los podemos mapear directamente.

map_paises = {"arica": "Chile", "shile": "Chile", "Iran, 2005 Qeshm earthquake": "Iran"}
eqk["pais"].map(map_paises).fillna(eqk["pais"])
0                Turkey
1          Turkmenistan
2            Azerbaijan
3           Azerbaijan 
4      Papua New Guinea
             ...       
215              China 
216        New Zealand 
225            Tanzania
226               japan
227               Chile
Name: pais, Length: 219, dtype: object

Para editarlo en el dataframe basta hacer un assign.

eqk = eqk.assign(pais=lambda x: x["pais"].map(map_paises).fillna(x["pais"]))

Ahora formatearemos los nombres, pasándolos a minúsculas y quitando los espacios al principio y final de cada string. Y ahabíamos hablado del ejemplo de Turkey.

eqk.loc[lambda x: x["pais"].apply(lambda s: "Turkey" in s), "pais"].unique()
array(['Turkey', 'Turkey '], dtype=object)
# Chaining method
eqk = eqk.assign(pais=lambda x: x["pais"].str.lower().str.strip())
eqk.loc[lambda x: x["pais"].apply(lambda s: "turkey" in s), "pais"].unique()
array(['turkey'], dtype=object)

Nota que no hay países con valores nulos porque ya fueron reparados.

eqk.loc[lambda x: x["pais"].isnull()]
año pais magnitud

¿Se puede sacar más provecho a los datos ?

No es posible crear variables nuevas o algo por el estilo, ya se hizo todo el procesamiento necesario para cumplir las reglas de negocio.

earthquakes.shape
(228, 4)
eqk.shape
(219, 3)

Dar respuesta

Como es un método de agregación podríamos simplemente hacer un groupby.

eqk.groupby(["pais", "año"])["magnitud"].max()
pais           año 
afghanistan    2000    6.3
               2001    5.0
               2002    7.3
               2003    5.8
               2004    6.5
                      ... 
turkmenistan   2000    7.0
united states  2001    6.8
               2003    6.6
venezuela      2006    5.5
vietnam        2005    5.3
Name: magnitud, Length: 134, dtype: float64

Sin embargo, en ocasiones, una tabla pivoteada es mucho más explicativa.

eqk.pivot_table(
    index="pais",
    columns="año",
    values="magnitud",
    aggfunc="max",
    fill_value=""
)
año 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011
pais
afghanistan 6.3 5 7.3 5.8 6.5 6.5
algeria 5.7 6.8 5.2 5.5
argentina 7.2 6.1
azerbaijan 6.8
bangladesh 5.6
burma 6.8
chile 6.3 8 7.7 8.8
china 5.9 5.6 5.5 6.3 5.3 5.2 5 6.1 7.9 5.7 6.9 5.4
colombia 6.5 5.9
costa rica 6.4 6.1
democratic republic of the congo 6.2 5.9
dominican republic 6.4
ecuador 5.5
el salvador 7.6
greece 6.2 6.4
guadeloupe 6.3
guatemala 6.4
haiti 7
india 7.6 6.5 5.1 5.3 5.1 6.9
indonesia 7.9 7.5 6.9 9.1 8.6 7.7 8.5 7.3 7.6
iran 5.3 6.5 6.6 6.3 6.4 6.1
italy 4.7 5.9 6.2
japan 6.1 6.8 8.5 6.6 6.6 6.7 6.9 6.4 9
kazakhstan 6
kyrgyzstan 6.9
martinique 7.4
mexico 7.5
morocco 6.3
mozambique 7
new zealand 5.4 6.6 6.3
nicaragua 5.4
pakistan 6.3 5.4 7.6 4.9 5.2 6.4
panama 6.5
papua new guinea 8 7.6 6.1
peru 8.4 7.5 8
philippines 7.5 6.5 7.1 5.3
russian federation 7.3 6.2
rwanda 5.3
samoa 8.1
serbia 5.7
slovenia 5.2
solomon islands 8.1
taiwan 6.4 7.1 5.2 7
tajikistan 5.2 5.6 5.2
tanzania 6.4 5.8 6.8
turkey 6 6.5 6.3 5.6 5.9 6.1 7.1
turkmenistan 7
united states 6.8 6.6
venezuela 5.5
vietnam 5.3

¿Notas las similitudes con groupby? Ambos son métodos de agregación, pero retornan formas de la matriz distintas.

Sin embargo, esto se vería mucho mejor con una visualización, que es lo que veremos en el próximo módulo.

import altair as alt
alt.themes.enable('opaque')

alt.Chart(
    eqk.groupby(["pais", "año"])["magnitud"].max().reset_index()
).mark_rect().encode(
    x='año:O',
    y='pais:N',
    color='magnitud:Q'
)

Resumen

  • En el mundo real te encontrarás con múltiples fuentes de datos, es importante adaptarse ya que las tecnologías cambian constantemente.

  • Datos deben entregar valor a través del análisis.

  • Es poco probable que los datos vengan “limpios”.

  • El análisis exploratorio de datos (EDA) es una metodología que sirve para asegurarse de la calidad de los datos.

  • A medida que se tiene más experticia en el tema, mejor es el análisis de datos y por tanto, mejor son los resultados obtenidos.

  • No existe un procedimiento estándar para realizar el EDA, pero siempre se debe tener claro el problema a resolver.