Cliente en Elixir para la API de Whatsapp Empresarial

En Resuelve llevamos usando Whatsapp empresarial desde sus primeras versiones. Decidimos desarrollar un cliente para poder comunicarnos con esta API y hacerla pública el día 24 de Enero del 2020, este cliente lo usamos actualmente para entregar en promedio más de ~20,000 mensajes y recibir ~15,000 mensajes, esto por día en 4 países.

Después de registrar el número en Facebook Business y levantar los contenedores requeridos en la documentación https://developers.facebook.com/docs/whatsapp, podrás usar el cliente desde algún proyecto de phoenix o simplemente desde la consola de Elixir.

El cliente cuenta con un GenServer (proceso) que se ejecuta cada 24 hrs para actualizar el header de autenticación.

defmodule MyApp.Application do
  use Application

  alias Whatsapp.Models.WhatsappProvider

  def start(_type, _args) do
    import Supervisor.Spec

    children = [
      worker(Whatsapp.Auth.Server, [[
        %WhatsappProvider{
          name: "resuelve-mx",
          url: "https://my-wa-instance.io:9090/v1",
          username: "username",
          password: "password"
        }
      ]])
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end


Después de definir uno ó más whatsapp´s se podrá comunicar con la api

iex> %{"contacts" => [%{"wa_id" => wa_id}] = WhatsappApi.check("5566295500", "My company")
iex> message = Whatsapp.Models.MessageOutbound.new(to: wa_id, body: "Hi!")
iex> WhatsappApi.send(message, "My company")


El uso de este GenServer es opcional, ya que se puede obtener el token y enviarlo en cada request independientemente, realizando el login primero.

iex> %{"users" => [%{"token" => token}]} = Whatsapp.Api.Users.login({"https://my-wa-instance.io:9090/v1", {"Authorization", "Basic #{base64UsernamePassword}"}})
iex> message = Whatsapp.Models.MessageOutbound.new(to: "wa_id", body: "Hi!")
iex> auth_header = [{"Authorization", "Bearer #{token}"}]
iex> Whatsapp.Api.Messages.send({"https://my-wa-instance.io:9090/v1", auth_header}, message)

Cualquier aportación es bienvenida con su respectivo Pull Request.
https://github.com/resuelve/wax

Equidad en modelos de machine learning

¿Mi sistema está representando éticamente el problema a solucionar?

Me resultó intrigante una línea de la serie “Dear white people”, en la que un personaje afrodescendiente clama que es importante que esta comunidad haga parte activa del ciclo de desarrollo de software, ya que si no hacen parte de esto el software va a estar sesgado. Esto se ha visto en casos de clasificación de imágenes, donde los países con menores ingresos económicos no tienen resultados de clasificación tan acertados como los de países con mayores ingresos económicos. Esto está estrechamente relacionado con el origen de los datos y las variables que son consideradas para la generación de estos modelos.

Y es que este tipo de implicaciones se ven por ejemplo en NLP, ya que como la mayor parte de investigación está en universidades angloparlantes, encontrar recursos en español es un poco más difícil. Esto además tiene otras implicaciones de carácter ético, como por ejemplo un modelo de clasificación para un sistema jurídico o para préstamos bancarios, ya que son modelos sensibles con respecto a estas poblaciones.

La clasificación de imágenes baja hasta en un 20% en países menos desarrollados

Por esta razón las tres métricas más usadas para medir la equidad, de acuerdo a un estudio realizado en Standford son:

  • Anti-clasificación: Variables como raza o género no deben ser usadas para realizar una clasificación.
  • Clasificación paritaria: Las métricas de desempeño debe ser la misma para las diferentes poblaciones.
  • Calibración: El score generado en un individuo con las ciertas características deben ser las mismas sin importar la población de la cual es obtenido.

Cabe notar que los mismos autores recalcan en los problemas matemáticos que representan estas métricas, ya que se trata de un área poco explorada.

Llevándolo al caso de uso empresarial, se realiza un modelo para predecir si a un cliente se le va a realizar una venta. Para probar que se cumpla la métrica de anti-clasificación al realizarse la creación del modelo se debe revisar que no se hayan usado variables sensibles como raza o género. Normalmente se tiene bastante control sobre esto, pero aún así vale la pena prestarle atención a variables compuestas o a scores generados por otros sistemas, como por ejemplo proveedores de correos.

Para probar que se haga una clasificación paritaria, se separan las poblaciones por estas variables y se les aplican las métricas de desempeño como área bajo la curva ROC, accuracy o F1. También vale destacar que el comparar visualmente las curvas ROC pueden ser de gran ayuda.

Finalmente se puede probar que la calibración se cumpla al tomar una muestra aleatoria y cambiarle los datos en las variables sensibles, y al comparar con la probabilidad de venta original no debería cambiar. Otra herramienta visual que puede generar retroalimentación sobre esta métrica es graficar un histograma de las probabilidades de cierre, y esta distribución debe ser bastante similar entre cada población.

Tutorial básico para publicar un módulo en Pypi

Normalmente el conocimiento base de todo desarrollador llega hasta la solución puntual de un problema, pero cuando este problema resulta siendo bastante común en proyectos propios, del equipo, empresa o ecosistema, entonces es un buen momento para crear una librería. En esta entrada vamos a dar un ejemplo de cómo crear un proyecto con ciertos criterios de calidad, pero al mismo tiempo básico.

En este tutorial vamos a crear una clase Snake en un módulo llamado animals, la cual va a estar documentada con Sphinx y con tests unitarios usando unittest, para ser publicada en pypi.

La clase Snake para este ejemplo va a ser

# snake.py

class Snake:
    def eat(self, animal):
        return 'Delicious!!!' if animal is 'mouse' else 'No thanks'

Estructura de archivos

A continuación vamos a mostrar la estructura propuesta, vamos a ir explicando la finalidad de los demás archivos.

 .
├ animals
|  ├ __init__.py
|  └ snake.py
├ setup.py
├ requirements.txt
├ README.md
└ .gitignore

En el módulo de animals está la clase en el archivo snake.py y un archivo para configurar el módulo, en donde se importan los archivos y clases a usar, como se muestra en el ejemplo

# __init__.py

from .snake import Snake

__name__ = 'animals'
__version__ = '0.0.0'
__all__ = ['Snake']

En el archivo requirements.txt se agregan las dependencias del proyecto, especificando versiones si es necesario, como por ejemplo

# requirements.txt

pandas==0.25.0
sklearn

Este archivo es tomado por el archivo de configuración del módulo, el cual es setup.py, para la configuración de dependencias. Este archivo puede ser de la siguiente forma

# setup.py

import setuptools
import animals

with open('README.md', 'r') as fh:
    long_description = fh.read()

setuptools.setup(
    name='animals',
    version=animals.__version__,
    author='Santa Claus',
    author_email='santa@claus.com',
    description='Please describe this',
    long_description=long_description,
    long_description_content_type='text/markdown',
    url='https://github.com/resuelve/animals',
    packages=setuptools.find_packages(exclude=['sphinx_docs', 'docs', 'tests']),
    python_requires='~=3.5',
    install_requires=[
        i.replace('\n', '')
        for i in open('requirements.txt', 'r').readlines()
    ],
    extras_require={
        'dev': ['setuptools', 'wheel', 'twine', 'Sphinx'],
    },
    classifiers=[
        'Development Status :: 3 - Alpha',
        'Intended Audience :: Developers',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Topic :: Software Development',
        'License :: OSI Approved :: MIT License',
        'Operating System :: OS Independent',
    ],
)

Para terminar, el archivo .gitignore puede ser el mismo generado por github, solo recomendamos que al menos estén estas reglas

# .gitignore

*.egg-info/
build/
dist/
__pycache__/

Pruebas unitarias

Se agregan unas pruebas unitarias usando unittesting. La idea es que sean lo más concisas posibles. Este archivo para este caso va a estar dentro de la carpeta tests.

# test_snake.py

import unittest
from animals.snake import Snake

class TestSnake(unittest.TestCase):

    def test_eat(self):
        snake = Snake()
        self.assertEqual(snake.eat('mouse'), 'Delicious!!!')
        self.assertEqual(snake.eat('elephant'), 'No thanks')


if __name__ == '__main__':
    unittest.main()

La forma de revisar que estén pasando todos los tests es corriendo en consola, el cual solo funciona si los tests están ubicados dentro de la carpeta de tests y comienzan con la palabra test_ , como por ejemplo, test_snake.py

python3 -m unittest tests/test_*

Documentación

Para generar una página web con los comentarios en python nosotros usamos Sphinx. Un ejemplo de la forma de documentar usando la estructura de sphinx se puede ver en la siguiente forma

# snake.py

class Snake:
    ''' Esta es la clase snake, de tener unos parámetros de constructor, acá irían'''
    
    def eat(self, animal):
        ''' Toda serpiente necesita comer
        :param animal: Hoy que animal va a comer
        :type animal: str
        :return: Si la serpiente está dispuesta a comer o no
        :rtype: str
        '''
        return 'Delicious!!!' if animal is 'mouse' else 'No thanks'

Para esto se agrega una carpeta donde se guarde la estructura de los archivos a generar. En este punto la estructura de carpetas debería estar de la siguiente manera

.
├ animals
|  ├ __init__.py
|  └ snake.py
├ tests
|  └ test_snake.py
├ sphinx_docs
|  ├ conf.py
|  ├ index.rst
|  └ snake.rst
├ setup.py
├ requirements.txt
├ MANIFEST.in
├ README.md
└ .gitignore

En conf.py está la configuración y demás formas de revisar el código para generar el HTML, en index.rst está la entrada inicial del proyecto y cómo se va a ver representada y en snake.rst están las directivas de cómo leer el archivo de snake.py

# conf.py

# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# http://www.sphinx-doc.org/en/master/config

# -- Path setup --------------------------------------------------------------

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('..'))


# -- Project information -----------------------------------------------------

project = 'animals'
copyright = '2019, Resuelve'
author = 'Santa Claus'

# The full version, including alpha/beta/rc tags
release = '0.0.0'


# -- General configuration ---------------------------------------------------

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.viewcode',
    'sphinx.ext.intersphinx',
    'sphinx.ext.autosummary',
]

# Include Python objects as they appear in source files
# Default: alphabetically ('alphabetical')
autodoc_member_order = 'bysource'
# Default flags used by autodoc directives
autodoc_default_flags = ['members', 'show-inheritance']
# This value contains a list of modules to be mocked up
autodoc_mock_imports = [
    # all the dependencies that you want to ignore
]
# Generate autodoc stubs with summaries from code
autosummary_generate = True

# Add any paths that contain templates here, relative to this directory.
# templates_path = ['_templates']

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []


# -- Options for HTML output -------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
# alabater theme opitons
html_theme_options = {
    'github_button': True,
    'github_type': 'star&v=2',
    'github_user': 'resuelve',
    'github_repo': 'silk-ml',
}

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named 'default.css' will overwrite the builtin 'default.css'.
html_static_path = ['_static']

# Sidebars configuration for alabaster theme

html_sidebars = {
    '**': [
        'about.html',
        'navigation.html',
        'searchbox.html',
    ]
}

# I don't like links to page reST sources
html_show_sourcelink = True

# Add Python version number to the default address to correctly reference
# the Python standard library
intersphinx_mapping = {'https://docs.python.org/3.7': None}
.. index.rst

*******
animals
*******

.. image:: https://img.shields.io/pypi/v/animals.svg
   :target: https://pypi.python.org/pypi/animals
   :alt: PyPI Version
.. image:: https://img.shields.io/pypi/pyversions/animals.svg
   :target: https://pypi.python.org/pypi/animals
   :alt: PyPI python Version

Simple Intelligent Learning Kit (SILK) for Machine learning

Welcome to silk_ml's documentation!
===================================

:Source code: `github.com project <https://github.com/resuelve/animals>`_
:Bug tracker: `github.com issues <https://github.com/resuelve/animals/issues>`_
:Generated: |today|
:License: MIT
:Version: |release|

Project Modules
===============

List of project's modules

.. toctree::
   :maxdepth: 2
   
   _autosummary/snake.rst

Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
.. snake.rst

snake
========

.. automodule:: animals.snake
   :members:

Generación de binarios

Primero se necesita instalar las herramientas necesarias para poder realizar la compilación del código

python3 -m pip install --upgrade setuptools wheel twine

Ya teniendo esto instalado, primero se compilan y crean los binarios usando el mismo archivo de configuración setup.py, y se suben a pypi usando twine. Vale destacar que primero es necesario haber creado una cuenta en pypi.

python3 setup.py sdist bdist_wheel
python3 -m twine upload dist/*


Elixir Mexico City 29 de Agosto

Este 29 de Agosto estaremos hosteando en Resuelve el meetup de Elixir Mexico City donde dos de nuestros ingenieros serán Speakers, la entrada es gratuita pero reserva en el evento en meetup.com.

Estaremos transmitiendo el evento así que síguenos en twitter o facebook.

¿Quienes estarán?

¿Dónde?
Calz. Gral. Mariano Escobedo 555

Pure Black GmbH
Graslitzer Str. 6
84478 Waldkraiburg
Tel. 08638 6964680
www.pureblack.de

AutoML | Una biblioteca simple para hacer modelos de Machine Learning.

Como científico de datos, siempre intentas obtener el mejor modelo para tu problema. Obtener un gran modelo, entrenarlo y ajustarlo a la perfección es solo una parte de un proyecto más grande. A la medida que vas exponiéndote a algunos proyectos de aprendizaje automático y leyendo algunos de los ciclos de desarrollo de los científicos de datos, descubres que hay un ciclo de desarrollo parecido. En general para nuestro equipo se ve así:

Como muchos en la escena tecnológica, he tenido una experiencia autodidacta en aprendizaje automático. Cuando me estaba introduciendo en esta muy joven práctica, y en todas las palabras de moda asociadas, encontré mucha información, libros, videos, publicaciones, MOOC y tutoriales sobre los algoritmos de creación de modelos. Pero, cuando llegas a construir esos modelos con tus datos reales; datos que no están completos, disponibles, estandarizados, actuales, y una lista de etcéteras que podemos resumir en … datos que no están limpios; te das cuenta de que la mayoría de tu tiempo la pasas limpiando tus conjuntos de datos. Mientras que la creación de tu modelo es algo que toma una fracción de ese tiempo. Adicionalmente, cuando terminas de entrenar esos modelos y deben estar disponibles para clientes/sistemas en producción, resulta que el video tutorial que viste en Coursera / Youtube o el repositorio de Github que te guiaba ya no contaba con esa sección.

De Bernard en StackExchange

No me malinterpreten, creo que encontrar una arquitectura adecuada y ajustar correctamente los hiperparámetros puede mejorar un modelo. Sin embargo, considero que es la calidad y la cantidad de datos lo que más impacta para hacer buenos modelos.

Además, aunque un modelo que solo se puede utilizar en una máquina local es un buen punto de partida, hay muchos pasos a seguir. Pero, como se comentó anteriormente, la limpieza, el aumento de datos y la implementación de un modelo no son temas tan populares como los algoritmos de aprendizaje automático.

Una y otra vez, el equipo y yo descubrimos que estábamos haciendo el mismo proceso de limpieza para nuestros conjuntos de datos, aún haciendo modelos completamente diferentes. Mismos métodos de limpieza para diferentes datos.

Descubrimos también que no era un problema exclusivo de nuestro equipo, sino un problema para muchos científicos de datos:

“The reason data scientists are hired in the first place is to develop algorithms and build machine learning models — and these are typically the parts of the job that they enjoy most. Yet in most companies today, 80 percent of a data scientist’s valuable time is spent simply finding, cleaning and reorganizing huge amounts of data. Without the right cloud tools, this task is insurmountable.” Infoworld

Así que, pensando como ingenieros, decidimos automatizar la parte repetitiva del trabajo, y no solo eso, sino todo lo que pudieramos del ciclo. Ahí fue cuando comenzó la idea de hacer AutoML.


Algunas consideraciones antes de comenzar…

Al momento de iniciar este proyecto, los modelos desarrollados por el equipo eran en su mayoría de datos estructurados. Puede pensarse como datos que pueden estar en una hoja de cálculo simple. Cosas como números, fechas y cadenas de texto. Contrario a los datos no estructurados (imágenes, audio, video, texto). Además, los modelos creados eran para resolver problemas de clasificación.

Photo by Jeremy Bishop on Unsplash

Obtén datos primero

La primera parte fue conseguir los datos con los que vamos a trabajar. Afortunadamente, ya habíamos centralizado la mayor parte de nuestros datos en un solo data warehouse, por lo que solo tuvimos que consultarlo. Entonces, lo primero que necesita la biblioteca para trabajar es la capacidad de hacer consultarlo.

Lavar y enjuagar

Una vez que tenemos los datos que necesitamos, es buena idea deshacerse de las cosas que pueden confundir el algoritmo en la etapa de entrenamiento. Para limpiar los datos utilizamos algunas técnicas realmente simples para excluir variables:

  • Eliminación de constantes
  • Valores faltantes dentro de un umbral de x%
  • variables con más de y categorías, por ejemplo.

Nos aseguramos de que todos los datos que obtengamos estén en el formato y tipo que esperamos. Por ejemplo, nos aseguramos de que las fechas estén en un formato correcto y no como cadenas de texto.

Datos ++

Una vez que hayamos limpiado los datos, avancemos un poco más y aumentemos las variables que tenemos con algunas transformaciones simples:

  • Discretización de variables categóricas
  • Sumas, multiplicaciones y uso de algunas expresiones lógicas
  • Extracciones de fecha: día /día de la semana /mes

Teniendo una buena relación

Ya tenemos una gran cantidad de datos para entrenar, pero para determinar si una variable es útil usamos un análisis de correlación para eliminar las variables que no están tan relacionadas con la variable objetivo (la información que queremos predecir), el factor de inflación de varianza (VIF) para reducir la multicolinealidad e identificamos posibles fugas de datos (data leakage).

Análisis de correlación — este es un método estadístico de evaluación utilizado para estudiar la fuerza de la relación lineal entre dos variables continuas (por ejemplo, altura y peso)

El factor de inflación de varianza (VIF) — es otro método estadístico de evaluación que cuantifica el grado de correlación entre un predictor y los otros predictores en un modelo.

Fuga de datos (data leakage) — cuando una característica está demasiado relacionada con la variable objetivo esta puede ser un mal predictor. Un tipo de data leakage común es el caso en que son datos que no están al momento que usarías tu modelo de predicción. (por ejemplo, tomó_antibiótico= True, cuando se quiere predecir enfermedad. La persona puede estar tomando medicina porque ya sabe que está enferma.)

Es importante realizar este tipo de controles para ofrecer a un modelo (por ejemplo, una red neuronal) una mejor oportunidad de rendir mejor y converger. También crea una pausa saludable para que el científico de datos reflexione sobre las variables seleccionadas y una oportunidad para reducir la dimensionalidad de las entradas para el modelo. Un modelo más simple es más fácil de entender y mantener.

Creación de modelos

Hasta este punto (si todo ha salido bien) deberíamos tener un conjunto de datos limpio con variables aumentadas y relacionadas con nuestra variable objetivo. ¡Genial!
Primero hemos encontrado que es muy útil para hacer un modelo de clasificación muy simple. Solo para tener un ‘modelo base’ que dibuje una línea en la arena como el mínimo estándar de desempeño.
Para buscar un modelo que pueda superar al modelo base, utilizamos tres estrategias:

  • Algoritmos genéticos evolutivos
  • Búsqueda aleatoria
  • Tpot

Sacamos al mejor modelo de cada estrategia y los evaluamos con algunas métricas:

  • AUC (área bajo la curva)
  • F1 score
  • MCC score

¡El que mejor desempeño tenga bajo estos parámetros es el ganador!

Compartamos con el equipo.

Para este momento hemos creado un modelo con los mejores datos, limpios y aumentados; con la mejor arquitectura y parámetros; además lo hemos comparado con con al menos otros tres modelos. ¡Muy bien!
Pero un modelo que se ejecuta en tu computadora no es de mucha ayuda para el equipo. Aún menos útil para la organización. Entonces, deberíamos desplegarlo donde todos puedan utilizarlo.

Actualmente, AutoML puede desplegar un modelo entrenado localmente en el motor de ML de Google, creando un servicio dedicado, auto-escalado y con un endpoint para atender todas las solicitudes que sean necesarias.

Pensamientos y trabajos futuros.

Si bien estamos muy orgullosos del resultado de AutoML, y lo hemos utilizado para desarrollar dos modelos en producción. Encontramos muchas áreas de oportunidad y ‘bugs’ que pueden (y deben) solucionarse para hacer un producto más robusto, más rápido y mejor para todos. Algunas ideas son:

  • Opción de hacer modelos regresivos.
  • Opción para hacer una mayor variedad de modelos (CNN, RNN, Arima, árboles de decisión, etc.)
  • Comprobación de correlación de datos más eficiente
  • Mayor variedad para comprobar correlación de datos
  • Implementación para otros proveedores de la nube
  • Despliegue agnóstico de infraestructura

AutoML es un trabajo en equipo de Francisco Serrano y Dudley Díaz (yo) en el equipo de AI en Resuelve tu Deuda en la Ciudad de México, México.

Descargar archivos csv

¿Qué es un archivo csv?

Un csv es un archivo de valores separados por comas. Se pueden usar con excel o con cualquier otro programa de hoja de cálculo. Se diferencian de otros tipos de archivo de hoja de cálculo en que no puede guardar ningún formato ni puede guardar las fórmulas.

Los archivos con extension csv tienen un comportamiento similar al que tienen los archivos png, jpg, pdf, el navegador los toma como archivos pequeños que puede mostrar en otra pestaña y no los descarga.

¿Cuál es el problema?

Los clientes cuando tratan de descargar un archivo en formato csv el navegador lo abre en una ventana nueva y no lo descarga.

Bien, un issue sencillo, nada complicado, ¿qué tan difícil es descargar un archivo csv?, y si, no es difícil descargar un csv, sin embargo, tuve que hacer una pequeña investigación antes de resolver este issue, y la quiero compartir con ustedes.

En vista de que el navegador no va a descargar el archivo, pero si muestra su contenido, lo primero hay que hacer es descargar el contenido del archivo que en nuestro caso es esta en AWS. Creamos una función que descargue el contenido usando fetch, a continuación con ayuda de la librería Downloadjs descargamos el archivo.

Downloadjs es una librería que permite descargar archivos en diferentes tipos de formatos. Recibe tres parámetros :

  • El archivo que se va a descargar.
  • El nombre del archivo y su extensión(archivo.csv)
  • El MIME type (Es una forma estandarizada de indicar la naturaleza y el formato de un documento) MDN

Este es el método rápido, sencillo y mágico para descargar un archivo csv.

Y así de fácil tienes tu archivo csv.

También puedes descargar un cvs de otra forma un poco mas compleja pero igual de efectiva.

De igual manera que antes descargamos el contenido del archivo con fetch,con new blobconcatenamos la información, le decimos el tipo de archivo que será, creamos una nueva url de la cual vamos a descargar el archivo csv. Creamos una etiqueta (<a></a>)y descargamos el archivo. Posteriormente eliminamos la url creada ya que el navegador no debe guardar la referencia al archivo.

Aqui puedes probar que funciona.

Para finalizar

Si usas el primer método te vas ahorrar código y también vas a poder descargar archivos jpg, png, pdf, txt, gif. De igual manera que con los .csv los puedes descargar muy fácil y rápido. Ten en cuanta que estas agregando una librería mas a tu código con lo que ello implica, aunque al final es una librería pequeña que hace algo puntual.

Si usas el segundo método es un poco mas de código, pero te estas evitando una librería mas en el código. Ahora si sabes que lo único que necesitas es descargar archivos .csv y no necesitas los otros formatos que mencioné anteriormente, pues para que agregar una librería con este método es suficiente.

¿Cuál es la mejor?

Depende de como estén trabajando, del equipo y de que librerías y/o frameworks estén utilizando.

Correcciones, mejoras, comentarios, siempre bien recibidos.

Saludos.

Facebook
Twitter
YouTube