Autenticación con Azure Active Directory B2C –End to End-

Hace poco tuve la necesidad de implementar un método de autenticación y autorización utilizando Azure Active Directory B2C, ya que este es un servicio relativamente nuevo en Azure me costó encontrar una documentación de inicio a fin -end to end- que ejemplifique mi necesidad. En este post veremos cómo implementar una autenticación con AADB2C en una aplicación web ASP.NET Core, una web API protegida, autorización mediante grupos y consumo del API de Microsoft Graph para consultar información de trabajo del usuario.

Requisitos

  • Cuenta en Azure 
  • Recurso de Azure Active Directory B2C
  • Visual Studio 2019

El código completo de este ejemplo puede ser descargado de github.com/dfmera

El diagrama de la solución que vamos a implementar es el siguiente

Arquitectura solución Active Directory B2C

Autenticación de una aplicación web con ASP.NET Core y AADB2C

Creación de una aplicación web con ASP.NET Core

Crear una aplicación web (MVC), .NET Core 3.1 o superior, sin autenticación y configurar para HTPS.

En Visual Studio creamos una aplicación web que será nuestro front y que tendrá la autenticación con Active Directory B2C posteriormente.

Aplicación ASP.Net Core Active Directory B2C

Instalar los siguientes paquetes Nuget necesarios:

  • Microsoft.AspNetCore.Session
  • Microsoft.Identity.Web
  • Microsoft.Identity.Web.UI
  • Newtonsoft.Json

Configurar el startup.cs para agregar todas las dependencias necesarias.

Configuración de sesiones y cookies.

services.AddDistributedMemoryCache();

services.Configure<CookiePolicyOptions>(options =>
{
    // This lambda determines whether user consent for non-essential cookies is needed for a given request.
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
    // Handling SameSite cookie according to https://docs.microsoft.com/en-us/aspnet/core/security/samesite
    options.HandleSameSiteCookieCompatibility();
});

Configuración de Azure Active Directory B2C

// Configuration to sign-in users with Azure AD B2C
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd");

Agregar la política de autenticación para los controladores y el controlador para la autenticación

services.AddControllersWithViews(options =>
{
    var policy = new AuthorizationPolicyBuilder()
    .RequireAuthenticatedUser()
    .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
services.AddRazorPages();

Agregar OpenID con la configuración de Azure -esta dependencia es específicamente necesaria para AADB2C, en una configuración de AAD no sería necesaria-

//Configuring appsettings section AzureAdB2C, into IOptions
            services.AddOptions();
            services.Configure<OpenIdConnectOptions>(Configuration.GetSection("AzureAd"));

En el método Configure() agregar las siguientes configuraciones -es importante agregar las lineas en el orden correcto-.

app.UseCookiePolicy();

app.UseAuthentication();
app.UseAuthorization();

En el método Configure() modificar el método app.UseEndPoints() de la siguiente forma:

app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
                endpoints.MapRazorPages();
            });

Modificar el HomeController para agregar la autenticación

En el HomeController agregar la notación ‘Authorize’

[Authorize]
public class HomeController : Controller
{

Agregar e instanciar las siguientes propiedades en el controlador.

private readonly ITokenAcquisition _tokenAcquisition;

private IConfiguration Configuration { get; }

public HomeController(ILogger<HomeController> logger, IConfiguration configuration, ITokenAcquisition tokenAcquisition)
{
    _logger = logger;
    _tokenAcquisition = tokenAcquisition;

    Configuration = configuration;
}

En el método Index() agregar la captura del usuario logueado.

var user = HttpContext.User;
ViewData["User"] = user;

Modificar las vistas para mostrar info del usuario logueado

Mostrar la info de los Claims del usuario logeado en la página Index.cshtml

@{
    var user = ViewData["User"] as ClaimsPrincipal;
}

<table class="table table-striped table-bordered table-condensed table-hover">
    <tr>
        <th>ClaimType</th>
        <th>Value</th>
    </tr>

    @foreach (var claim in user.Claims)
    {
        <tr>
            @{
                if (claim.Type == "groups")
                {
                    <td><b>@claim.Type</b></td>
                }
                else
                {
                    <td>@claim.Type</td>
                }
            }
            <td>@claim.Value</td>
        </tr>
    }
</table>

Agregar una página plantilla para el login y logout del usuario que se llame _LoginPartial

@using Microsoft.Identity.Web
@if (User.Identity.IsAuthenticated)
{
<ul class="nav navbar-nav navbar-right">
    <li class="navbar-text">Hello @User.GetDisplayName()!</li>
    <li class="navbar-btn">
        <form method="get" asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="EditProfile">
            <button type="submit" class="btn btn-primary" style="margin-right:5px">Edit Profile</button>
        </form>
    </li>
    <li><a asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignOut">Sign out</a></li>
</ul> }
            else
            {
<ul class="nav navbar-nav navbar-right">
    <li><a asp-area="MicrosoftIdentity" asp-controller="Account" asp-action="SignIn">Sign in</a></li>
</ul>}

Agregar la página parcial en la página _Layout

<partial name="_LoginPartial" />

Registrar la aplicación cliente (front) en Azure Active Directory B2C

En el portal de Azure, seleccionar el inquilino creado con AADB2C, en la sección “Registro de Aplicaciones” crear una nueva aplicación de la siguiente forma:

Aplicación Active Directory B2C

En el registro de la aplicación, en la sección “Autenticación”, agregar una plataforma web o SPA y agregar las url de sign in y sign out, en este caso colocaremos las páginas que por defecto crea la librería Microsoft.Identity.Web en la aplicación ASP.NET Core

Autenticación Active Directory B2C

Habilitar los tokens de acceso y token id en la opción de “Autenticación”

Tokens Active Directory B2C

En la sección “Información General”, copiar los ID de cliente (aplicación) e inquilino (directorio) para configurarlos en la aplicación.

Ids Active Directory B2C

Copiar los nombres de los flujos de usuario o políticas del AADB2C para configurarlos en la aplicación. Esta es la única sección que no se incluye en este post, para crear un flujo de usuario se puede seguir las instrucciones en este video: Cómo crear una política de usuario.  

Politicas Active Directory B2C

Configurar la aplicación ASP.NET Core

En el archivo appsettings.json agregar la siguiente configuración con la info de AADB2C.

"AzureAd": {
    "Instance": "https://{tenant}.b2clogin.com",
    "Domain": "{tenant}.onmicrosoft.com",
    "ClientId": "...0fe347",
    "TenantId": "...16dc876",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath ": "/signout-callback-oidc",
    "SignUpSignInPolicyId": "B2C_1_...",
    "ResetPasswordPolicyId": "B2C_1_...",
    "EditProfilePolicyId": "B2C_1_...",
    // To call an API
    "ClientSecret": ""
  }

Ejecutar la aplicación

Al ejecutar la aplicación deberá verse como la imagen a continuación, asegurarse que la aplicación corra en el mismo puerto con el que se configuró el registro en AADB2C.

Ejemplo Active Directory B2C

Autenticación de una API Rest con ASP.NET Core y AADB2C

Creación de una aplicación web API con ASP.NET Core

Crear una aplicación Web API (MVC), .NET Core 3.1 o superior, sin autenticación y configurar para HTPS

En Visual Studio creamos una aplicación web API que será nuestro backend y que tendrá la autenticación con Active Directory B2C posteriormente.

API Active Directory B2C

Instalar los paquetes Nuget necesarios:

  • Microsoft.Identity.Web
  • Microsoft.Identity.Web.UI

Configurar el startup.cs para agregar todas las dependencias necesarias

Agregar autenticación de API con Azure Active Directory B2C.

// Adds Microsoft Identity platform (AAD v2.0) support to protect this Api
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(options =>
        {
            Configuration.Bind("AzureAdB2C", options);

            options.TokenValidationParameters.NameClaimType = "name";
        },
options => { Configuration.Bind("AzureAdB2C", options); });

En el método Configure() agregar las siguientes configuraciones, asegurarse de que estén en el orden correcto.

app.UseAuthentication();
app.UseAuthorization();

Modificar el WeatherForecastController (API Rest creado por la plantilla) para agregar la autenticación

En el HomeController agregar la notación “Authorize”.

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{

Agregar la siguiente propiedad a la clase.

// The Web API will only accept tokens 1) for users, and 2) having the "access_as_user" scope for this API
static readonly string[] scopeRequiredByApi = new string[] { "access_as_user" };

En el método Get agregar la siguiente línea para permitir invocaciones solo para los ámbitos (scope) definidos en la variable scopeRequiredByApi.

public IEnumerable<WeatherForecast> Get()
{
    HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

Registrar la aplicación API (backend) en Azure Active Directory B2C

En el portal de Azure, seleccionar el inquilino creado con AADB2C, en la sección “Registro de Aplicaciones” crear una nueva aplicación de la siguiente forma:

Registro API Active Directory B2C

Habilitar los tokens de acceso y token id en la opción de “Autenticación”. Al estar registrando una API no es necesario especificar las url de inicio y cierre de sesión.

Tokens API Active Directory

Exponer una API y un ámbito en el registro de la aplicación en la opción de “Exponer API”

API scope Active Directory B2C

En la sección “Información General”, copiar los ID de cliente (aplicación) e inquilino (directorio) para configurarlos en la aplicación.

Configurar la aplicación ASP.NET Core

En el archivo appsettings.json agregar la siguiente configuración con la info de AADB2C

"AzureAdB2C": {
    "Instance": "https://{tenant}.b2clogin.com",
    "Domain": "{tenant}.onmicrosoft.com",
    "ClientId": "...eec754ca987f",
    "TenantId": "...a867e16dc876",
    "SignedOutCallbackPath ": "/signout/B2C_1_InicioSesion",
    "SignUpSignInPolicyId": "B2C_1_InicioSesion",
    "ResetPasswordPolicyId": "B2C_1_OlvidoContrasena",
    "EditProfilePolicyId": "B2C_1_EditarPerfil", // Optional profile editing policy
    //"CallbackPath": "/signin/B2C_1_sign_up_in"  // defaults to /signin-oidc
    "ClientSecret": ""
  }
Consumo de la API desde la App Web cliente (front)
Consumo API Active Directory B2C

En el registro de la aplicación cliente (front) agregar los permisos a la API expuesta

En el registro de la aplicación cliente agregar la API y el ámbito (scope) en la opción “Permisos de API”. Esto permitirá que un usuario logueado en la aplicación front tenga también permisos para invocar al API que creamos y protegimos en el paso anterior.

Permisos API Active Directory B2C

Luego de esto se debe conceder permisos con la opción “Conceder consentimiento de administrador para {tenant}”

Agregar un secreto en la opción “Certificados y Secretos” y copiar el valor generado. Este secreto es el equivalente a una clave con la que la aplicación web (front) solicitará acceso para ejecutar la API

Secreto Active Directory B2C

Configurar la aplicación web cliente para que pueda consumir la API

En el archivo appsettings.json agregar el secreto generado y copiado anteriormente en la sección con los valores de Active Directory.

"ClientSecret": "8xcP.KsnfG3l5HUXRU_r~.Bc~XXXXXXX"

Agregar la configuración con la URL de la API y el ámbito (scope) previamente agregado en el registro de la API en Active Directory B2C.

"TestApi": {
    "Scope": "https://{tenan}.onmicrosoft.com/{id}/access_as_user",
    "TestApiBaseAddress": "http://localhost:49157"
  },

En el archivo startup.cs modificar la configuración de Azure Active Directory B2C para agregar el API de backend de la siguiente forma:

// Configuration to sign-in users with Azure AD B2C
services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd")
        .EnableTokenAcquisitionToCallDownstreamApi(new string[] { Configuration["TestApi:Scope"] })
        .AddInMemoryTokenCaches();

En el controlador HomeController agregar el siguiente método PrepareAuthenticatedClient() para crear un objeto HttpClient y agregar a la cabecera un token generado para el consumo de la API.

private async Task<HttpClient> PrepareAuthenticatedClient()
{
    try
    {
        var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { Configuration["TestApi:Scope"] });
        Debug.WriteLine($"access token-{accessToken}");
        HttpClient _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        return _httpClient;
    }
    catch (Exception ex)
    {
        return new HttpClient();
    }
}

Agregar el siguiente método GetTest() para realizar la llamada a la API y devolver el resultado.

private async Task<string> GetTest(HttpClient httpClient)
{
    try
    {
        string testApiBaseAdress = Configuration["TestApi:TestApiBaseAddress"];
        var response = await httpClient.GetAsync($"{ testApiBaseAdress}/weatherforecast");

        if (response.StatusCode == HttpStatusCode.OK)
        {
            var content = await response.Content.ReadAsStringAsync();
            return content;
        }
        else
        {
            return response.StatusCode.ToString();
        }
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
}

En el método Index() agregar el siguiente código para el llamado a la API.

//CALL API
var httpClient = PrepareAuthenticatedClient().GetAwaiter().GetResult();
var resultApi = GetTest(httpClient).GetAwaiter().GetResult();
ViewData.Add("ResultApi", resultApi);

Modificar el archivo Index.cshtml para mostrar el resultado del llamado a la API en pantalla.

<table class="table table-striped table-bordered table-condensed table-hover">
    <tr>
        <td colspan="2">Test API</td>
        <td colspan="2"></td>
    </tr>

    <tr>
        <td colspan="2">Response</td>
        <td colspan="2">@ViewData["ResultApi"]</td>
    </tr>
</table>

Ejecutar las aplicaciones Web y Web API para probar la invocación a la API

Al ejecutar la aplicación, si todo es correcto, el resultado deberá verse como la imagen.

Resultado API Active Directory B2C

Consultar datos del usuario autenticado usando Graph desde la web API

Consumo de un método de Graph desde la web API

Agregar los permisos al consumo de Graph en el registro de la API en Active Directory B2C

En el registro de la API, en la sección “Permisos de API” agregar permisos al consumo de Graph de la siguiente forma:

Graph Active Directory B2C

Agregar permisos para los ámbitos GroupMember.Read.All y User.Read.All y conceder consentimiento de administrador con la opción “Conceder Consentimiento de Administrador para {tenant}”

Agregar un secreto en la opción “Certificados y Secretos” y copiar el valor generado.

Secreto API Active Directory B2C

Agregar el consumo de Graph en la aplicación Web API

En la aplicación Web API agregar los siguientes paquetes Nuget.

  • Microsoft.Graph
  • Microsoft.Graph.Auth

Agregar una clase GraphHelper para colocar los métodos para obtener información del usuario autenticado

Agregar el siguiente método estático para consultar los grupos a los que pertenece el usuario autenticado.

private static async Task<Dictionary<string, string>> ProcessUserGroupsB2C(GraphServiceClient graphClient, string userId)
{
    Dictionary<string, string> groupClaims = new Dictionary<string, string>();
    try
    {
        // Before instatntiating GraphServiceClient, the app should have granted admin consent for 'GroupMember.Read.All' permission.
        //var graphClient = context.HttpContext.RequestServices.GetService<GraphServiceClient>();

        if (graphClient == null)
        {
            Console.WriteLine("No service for type 'Microsoft.Graph.GraphServiceClient' has been registered in the Startup.");
        }
        else
        {

            // The properties that we want to retrieve from MemberOf endpoint.
            string select = "id,displayName,onPremisesNetBiosName,onPremisesDomainName,onPremisesSamAccountNameonPremisesSecurityIdentifier";

            IUserMemberOfCollectionWithReferencesPage memberPage = new UserMemberOfCollectionWithReferencesPage();
            try
            {
                //Request to get groups and directory roles that the user is a direct member of.
                memberPage = await graphClient.Users[userId].MemberOf.Request().Select(select).GetAsync().ConfigureAwait(false);
            }
            catch (Exception graphEx)
            {
                var exMsg = graphEx.InnerException != null ? graphEx.InnerException.Message : graphEx.Message;
                Console.WriteLine("Call to Microsoft Graph failed: " + exMsg);
            }

            if (memberPage?.Count > 0)
            {
                // There is a limit to number of groups returned, below method make calls to Microsoft graph to get all the groups.
                var allgroups = ProcessIGraphServiceMemberOfCollectionPage(memberPage);

                if (allgroups?.Count > 0)
                {
                    // Re-populate the `groups` claim with the complete list of groups fetched from MS Graph
                    foreach (Group group in allgroups)
                    {
                        // The following code adds group ids to the 'groups' claim. But depending upon your reequirement and the format of the 'groups' claim selected in
                        // the app registration, you might want to add other attributes than id to the `groups` claim, examples being;

                        // For instance if the required format is 'NetBIOSDomain\sAMAccountName' then the code is as commented below:
                        // groupClaims.Add(group.OnPremisesNetBiosName+"\\"+group.OnPremisesSamAccountName));
                        groupClaims.Add(group.Id, group.DisplayName);
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }

    return groupClaims;
}

Agregar el siguiente método para obtener la información de trabajo del usuario.

private static async Task<Dictionary<string, string>> ProcessUserJobInfoB2C(GraphServiceClient graphClient, string userId)
{
    Dictionary<string, string> Jobinfo = new Dictionary<string, string>();
    try
    {

        if (graphClient == null)
        {
            Console.WriteLine("No service for type 'Microsoft.Graph.GraphServiceClient' has been registered in the Startup.");
        }
        else
        {
            Microsoft.Graph.User result = new User();
            try
            {
                result = await graphClient.Users[userId]
                        .Request().Select(e => new
                        {
                            e.DisplayName,
                            e.Id,
                            e.Identities,
                            e.JobTitle,
                            e.CompanyName,
                            e.Department
                        }).GetAsync();
            }
            catch (Exception graphEx)
            {
                var exMsg = graphEx.InnerException != null ? graphEx.InnerException.Message : graphEx.Message;
                Console.WriteLine("Call to Microsoft Graph failed: " + exMsg);
            }

            Jobinfo.Add("CompanyName", result.CompanyName);
            Jobinfo.Add("Department", result.Department);
            Jobinfo.Add("JobTitle", result.JobTitle);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }

    return Jobinfo;
}

Agregar el siguiente método que obtiene la info de los grupos y del trabajo del usuario y los retorna en un solo diccionario.

public static async Task<Dictionary<string, string>> GetSignedInUsersGroupsB2C(GraphServiceClient graphClient, string userId)
{
    Dictionary<string, string> groupClaims = new Dictionary<string, string>();

    groupClaims = await ProcessUserGroupsB2C(graphClient, userId);

    var jobInfo = await ProcessUserJobInfoB2C(graphClient, userId);
    foreach (var item in jobInfo)
    {
        groupClaims.Add(item.Key, item.Value);
    }

    return groupClaims;
}

En el controlador WeatherForecastController agregar el siguiente método para hacer el llamado al Graph y devolver la info de los grupos y del trabajo del usuario.

[HttpGet("GetGraph")]
public Dictionary<string,string> GetGraph()
{
    //HttpContext.VerifyUserHasAnyAcceptedScope(scopeRequiredByApi);

    var user = HttpContext.User;
    
    var configAz = Configuration.GetSection("AzureAdB2C");

    IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
        .Create(configAz.GetSection("ClientId").Value)
        .WithTenantId(configAz.GetSection("TenantId").Value)
        .WithClientSecret(configAz.GetSection("ClientSecret").Value)
        .Build();
    ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);

    // Set up the Microsoft Graph service client with client credentials
    GraphServiceClient graphClient = new GraphServiceClient(authProvider);
    var userId = user.Claims.Where(x => x.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").Select(x => x).FirstOrDefault().Value;

    var grupos = GraphHelper.GetSignedInUsersGroupsB2C(graphClient, userId).GetAwaiter().GetResult();

    return grupos;
}

En el archivo appsettings.json agregar el secreto generado y copiado anteriormente en la sección con la configuración de Active Directory B2C.

"ClientSecret": "EF.6.6A09_Bq3AgEaXXXXXXXX"
Consumo de la API desde la aplicación web (front)

Agregar el llamado al nuevo método en la API desde la aplicación Web

En el HomeController agregar el siguiente método GetTestGraph para hacer el llamado al nuevo método en la API.

private async Task<Dictionary<string, string>> GetTestGraph(HttpClient httpClient)
{
    try
    {
        string testApiBaseAdress = Configuration["TestApi:TestApiBaseAddress"];
        var response = await httpClient.GetAsync($"{ testApiBaseAdress}/weatherforecast/GetGraph");
        if (response.StatusCode == HttpStatusCode.OK)
        {
            var content = await response.Content.ReadAsStringAsync();
            var result = JsonConvert.DeserializeObject<Dictionary<string, string>>(content);

            return result;
        }
        else
        {
            return new Dictionary<string, string>();
        }
    }
    catch (Exception ex)
    {
        return new Dictionary<string, string>();
    }
}

En el método Index() agregar el llamado al método GetTestGraph()

//CALL API GRAPH
var resultApiGraph = GetTestGraph(httpClient).GetAwaiter().GetResult();
ViewData.Add("ResultApiGraph", resultApiGraph);

Modificar la página Index.cshtml para mostrar los resultados devueltos por la API.

@{
    Dictionary<string, string> resultApiGraph = new Dictionary<string, string>();

    if (ViewData.ContainsKey("ResultApiGraph"))
    {
        resultApiGraph = ViewData["ResultApiGraph"] as Dictionary<string, string>;
    }

}

<table class="table table-striped table-bordered table-condensed table-hover">
    <tr>
        <td colspan="2">API Graph test</td>
        <td colspan="2"></td>
    </tr>

    @if (resultApiGraph != null)
    {
        @foreach (var group in resultApiGraph)
        {
            <tr>
                <td colspan="2">@group.Key</td>
                <td colspan="2">@group.Value</td>
            </tr>
        }
    }
</table>

Probar la aplicación

Ejecutar tanto la aplicación Web (Front) como la aplicación Web API (backend).

Ejemplo Graph Active Directory B2C

Artículos relacionados

Data Storytelling | La importancia de saber contar historias

Machine Learning | Predecir el valor de una acción usando series de tiempo

Las series de tiempo (time series) son muy útiles para predecir valores numéricos para uno o varios período de tiempo. En este ejemplo veremos cómo predecir el valor de una acción usando series de tiempo y ML.NET.

El código completo y el dataset que veremos en este ejemplo se puede descargar de GitHub

Problema

El valor de una acción en el mercado de valores se suele observar en gráficos de velas japonesas en sesiones de tiempo de: una hora, cuatro horas o un día. Durante cada sesión se grafica los valores de apertura, el valor más bajo, el valor más alto y el valor de cierre de la sesión. Como ejemplo veamos un gráfico del índice Nasdaq en sesiones de 4 horas (h4). Con los datos de este gráfico intentaremos predecir los valores del Nasdaq para la última sesión del día.

Machine Learning Gráfico H4

En resumen, intentaremos contestar las siguientes preguntas respecto al valor del Nasdaq: ¿Cuál va a ser el valor más alto que alcance de la acción durante la última sesión? ¿Cuál va a ser el valor más bajo que alcance la acción durante la última sesión? ¿Cuál va a ser el valor de cierre de la sesión?

Datos

Para este ejercicio contamos con un dataset con los valores de apertura, alto, bajo y cierre para cada sesión de cuatro horas del indice Nasdaq durante los últimos 6 meses. Los datos tienen esta apariencia

Machine Learning Data Acciones
Modelo

La técnica para analizar los datos que vamos a usar en este ejemplo es el análisis de serie temporal de variante única. El análisis de serie temporal (time series) de variante única examina una única observación numérica durante un período de tiempo a intervalos específicos, como los valores de una acción cada 4 horas.

El algoritmo que se usa en este tutorial es el análisis de un solo espectro (SSA). SSA sirve para descomponer una serie temporal en un conjunto de componentes principales. Estos componentes se pueden interpretar como partes de una señal que corresponde a tendencias, ruido, estacionalidad y muchos otros factores. Después, estos componentes se reconstruyen y se usan para pronosticar valores en el futuro.

Para la creación del modelo creamos usaremos .NET Core 3.1 y ML.NET , para lo cual iniciaremos creando un proyecto de librería de clases que llamaremos MLStockFunctionValuesML.Model. Al proyecto agregaremos los paquetes NuGet de Microsoft.ML y Microsoft.ML.TimeSeries.

Ahora crearemos las estructura necesaria para cargar el dataset de entrenamiento. Agregamos una clase ModelInput con el siguiente código

public class ModelInput
{
    [ColumnName("OpenTime"), LoadColumn(0)]
    public DateTime OpenTime { get; set; }

    [ColumnName("Open"), LoadColumn(1)]
    public float Open { get; set; }

    [ColumnName("High"), LoadColumn(2)]
    public float High { get; set; }

    [ColumnName("Low"), LoadColumn(3)]
    public float Low { get; set; }

    [ColumnName("Close"), LoadColumn(4)]
    public float Close { get; set; }

    [ColumnName("TickVolume"), LoadColumn(5)]
    public float TickVolume { get; set; }

}

A diferencia de un modelo de clasificación o de regresión, en un modelo de series de tiempo no realizamos la predicción en base a nuevos datos de entrada, la predicción se realiza en base a los datos de entrenamiento y para un período de tiempo -horizonte- determinado. Para la creación del modelo y predicción crearemos una clase ModelBuilder a nuestro proyecto.

En la clase agregamos un método llamado CreateModel y recibirá como parámetro el valor de la sesión a predecir: cierre, alto o bajo

/// <summary>
/// 
/// </summary>
/// <param name="value">Close,High,Low,Open</param>
public static ModelOutput CreateModel(string value)
{

}

Ahora seguiremos los pasos tradicionales en la creación de un modelo de Machine Learning. Primero cargamos los datos de entrenamiento, en este caso el dataset está en un archivo de texto csv. Adicionalmente separamos los últimos 60 registros (las últimas dos semanas) para evaluar el modelo.

// Load Data
IDataView trainingDataView = mlContext.Data.LoadFromTextFile<ModelInput>(
                                path: TRAIN_DATA_FILEPATH,
                                            hasHeader: true,
                                            separatorChar: ',',
                                            allowQuoting: true,
                                            allowSparse: false);

var trainList = mlContext.Data.CreateEnumerable<ModelInput>(trainingDataView, true);
int trainSize = trainList.Count();

var testList = trainList.TakeLast(60);

IDataView testDataView = mlContext.Data.LoadFromEnumerable(testList);

La creación del pipeline es muy simple para las series de tiempo ya que no necesitamos de transformaciones de datos, solo debemos agregar el algoritmo ForecastBySsa y especificar los parámetros deseados.

var forecastingPipeline = mlContext.Forecasting.ForecastBySsa(
                    outputColumnName: "ForecastedValue",
                    inputColumnName: value,
                    windowSize: 5,
                    seriesLength: 60,
                    trainSize: trainSize,
                    horizon: 1,
                    confidenceLevel: 0.95f,
                    confidenceLowerBoundColumn: "LowerBoundValue",
                    confidenceUpperBoundColumn: "UpperBoundValue");

El parámetro outputColumnName corresponde al nombre de la columna en el esquema en la que se colocará el valor de la predicción. inputColumnName es la columna en el esquema en los datos de entrenamiento con el valor a predecir, en nuestro caso es el parámetro que recibe el método CreateModel. En el parámetro trainSize le pasamos el total de los registros del dataset de entrenamiento, no queremos excluir ningún registro. seriesLength es el intervalo de la serie temporal en el que se dividen los datos para ser analizados en las ventanas de tiempo especificadas en windowSize. En nuestro caso queremos que los datos sean analizados en períodos de 60 registros (dos semanas de sesiones de 4 horas cada una) y que se tome en cuenta los últimos 5 registros (las primeras 5 sesiones del día) para predecir el siguiente valor de la serie (la última sesión del día). Si quisiera predecir más series en adelante lo debo especificar en el parámetro horizon.

Una vez creado el pipeline el siguiente paso es entrenar el modelo con el dataset de entrenamiento

SsaForecastingTransformer forecaster = forecastingPipeline.Fit(trainingDataView);

Para la evaluación del modelo creamos un método Evaluate

static void Evaluate(IDataView testData, ITransformer model, MLContext mlContext, string value)
{

}

Primero usamos el método Transform para evaluar los resultados con el dataset de pruebas.

IDataView predictions = model.Transform(testData);

Ahora debemos comparar el resultado de las predicciones con los resultados reales, para esto necesitamos primero crear una clase ModelOutput para poder instanciarla con el resultado de las predicciones.

public class ModelOutput
{
    public float[] ForecastedValue { get; set; }

    public float[] LowerBoundValue { get; set; }

    public float[] UpperBoundValue { get; set; }
}

El nombre de las propiedades tiene que coincidir con el nombre de las columnas de salida que especificamos en el pipeline anteriormente. Y deben ser de tipo float[] ya que según el parámetro Horizon se pueden realizar predicciones para una o más períodos hacia adelante.

De vuelta en el método Evaluate transformaremos las predicciones y los datos de prueba a un IEnumerable<> para poder compararlos entre si.

IEnumerable<float> actual =
         mlContext.Data.CreateEnumerable<ModelInput>(testData, true)
         .Select(observed =>
                {
                    switch (value)
                    {
                        case "Close":
                            return observed.Close;
                        case "High":
                            return observed.High;
                        case "Low":
                            return observed.Low;
                        default:
                            return observed.Close;
                    }

                });

IEnumerable<float> forecast =
            mlContext.Data.CreateEnumerable<ModelOutput>(predictions, true)
                .Select(prediction => prediction.ForecastedValue[0]);

El error de cada predicción lo calculamos restando el valor real y el valor de la predicción. Y evaluaremos el error con las siguientes métricas.

  • Error medio absoluto (Mean Absolute Error): mide la cercanía de las predicciones respecto del valor real. Este valor va de 0 a infinito. Cuanto más se acerque a 0, mejor es la calidad del modelo.
  • Error cuadrático medio (Root Mean Squared Error): resume el error del modelo. Este valor va de 0 a infinito.Cuanto más se acerque a 0, mejor es la calidad del modelo.
var metrics = actual.Zip(forecast, (actualValue, forecastValue) => actualValue - forecastValue);

var MAE = metrics.Average(error => Math.Abs(error)); // Mean Absolute Error
var RMSE = Math.Sqrt(metrics.Average(error => Math.Pow(error, 2))); // Root Mean Squared Error

Console.WriteLine("Evaluation Metrics");
Console.WriteLine("---------------------");
Console.WriteLine($"Mean Absolute Error: {MAE:F3}");
Console.WriteLine($"Root Mean Squared Error: {RMSE:F3}\n");

De vuelta en le método CreateModel agregamos la llamada al método Evaluate, con eso el modelo está completo y lo podemos grabar con el siguiente código

Evaluate(testDataView, forecaster, mlContext, value);

var forecastEngine = forecaster.CreateTimeSeriesEngine<ModelInput, ModelOutput>(mlContext);

forecastEngine.CheckPoint(mlContext, MODEL_FILE);

El último paso es la creación de un método para realizar la predicción del valor de la acción para el siguiente período de 4 horas. Para esto agregamos en método llamado Forecast.

static ModelOutput Forecast(IEnumerable<ModelInput> testData, int horizon, TimeSeriesPredictionEngine<ModelInput, ModelOutput> forecaster, MLContext mlContext, string value)
{

}

La predicción la hacemos con el método Predict. A diferencia de un modelo de clasificación o regresión, en las series de tiempo no es necesario pasar como parámetro una nueva observación al método Predict ya que la predicción se realiza en base a los datos de entrenamiento.

ModelOutput forecast = forecaster.Predict();

Solo a manera de ayuda agregamos una comparación de la predicción con el último registro de los datos de prueba con el siguiente código.

IEnumerable<string> forecastOutput =
    testData.TakeLast(horizon)
        .Select((ModelInput values, int index) =>
        {
            string rentalDate = values.OpenTime.ToString();
            float actualValue;
            switch (value)
            {
                case "Close":
                    actualValue = values.Close;
                    break;
                case "High":
                    actualValue = values.High;
                    break;
                case "Low":
                    actualValue = values.Low;
                    break;
                default:
                    actualValue = values.Close;
                    break;
            }

            float lowerEstimate = forecast.LowerBoundValue[index];
            float estimate = forecast.ForecastedValue[index];
            float upperEstimate = forecast.UpperBoundValue[index];
            return $"Date: {rentalDate}\n" +
            $"Actual {value}: {actualValue}\n" +
            $"Forecast: {estimate}\n";
         });

Console.WriteLine("Stock Forecast");
Console.WriteLine("---------------------");
foreach (var prediction in forecastOutput)
{
    Console.WriteLine(prediction);
}

return forecast;

De vuelta en el método CreateModel agregamos la última linea para realizar y retornar la predicción.

return Forecast(testList, 1, forecastEngine, mlContext, value);

Ahora solo nos queda probar el modelo, para esto creamos una aplicación de consola que llamaremos MLStockFunctionValues.ConsoleApp y agregamos la referencia al proyecto MLStockFunctionValuesML.Model.

En el Main de la aplicación bastará con agregar tres llamadas al método CreateModel, uno para predecir el valor del cierre de la siguiente sesión (siguiente período), otro para predecir el valor más bajo de la sesión y la última para predecir el valor más alto que alcance el precio durante la sesión.

var predictClose = ModelBuilder.CreateModel("Close");
var predictHigh = ModelBuilder.CreateModel("High");
var predictLow = ModelBuilder.CreateModel("Low");

La salida de la aplicación se verá de esta forma

Machine Learning Predicción de acciones

El código completo y el dataset que veremos en este ejemplo se puede descargar de GitHub

Artículos relacionados

Cómo hacer predicciones en batch usando ML.NET

Trabajar con múltiples fuentes de datos usando ML.NET

Machine Learning | Cómo hacer predicciones en batch usando ML.NET

El framework ML.NET es de gran utilidad para crear aplicaciones con Machine Learning de punta a punta -end to end-, es decir, desde el entrenamiento de un modelo con datos históricos hasta la inferencia o predicción de un resultado con nuevos datos. Cuando creamos una aplicación utilizando ML.NET Model Builder podemos hacer predicciones de un resultado o una predicción a la vez, pero hay ocaciones en que necesitamos hacer muchas predicciones a la vez en forma de batch. En este post haremos un ejercicio para crear una aplicación que predice la cancelación de una reserva de hotel y modificaremos el código para hacer predicciones en batch a partir de varios registros en un archivo .csv.

Problema

Tenemos un dataset con el histórico de las reservas de varios hoteles y el registro de los cambios que tuvo la reserva. Con estos datos vamos a crear un modelo de Machine Learning para predecir si una reserva puede ser cancelada, modificada o no tener cambios. Este escenario puede ser útil para ofrecer un seguro de cancelación de reserva a un menor o mayor costo según el resultado de la predicción.

Dataset

Contamos con un Dataset tomado de kaggle y modificado para marcar tres categorías en la columna de reservation_status, esta puede tener los valores: Booking-Changed; Canceled; Check-Out

Solución

Crearemos un modelo de Machine Learning usando el framework ML.NET. Ya que son tres categorías las que queremos predecir (Booking-Changed; Canceled; Check-Outdebemos crear un modelo de clasificación multiclase (multiclass classification).

El proyecto completo se puede descargar del repositorio de GitHub

Usaremos ML.NET Model Builder para crear la aplicación -para conocer más sobre ML.NET puedes ver este post aquí-. Seguiremos los siguientes pasos para crear la aplicación.

  • Primero creamos en Visual Studio 2019 un proyecto ASP.NET Core MVC en el cual crearemos una simple interfaz para cargar un archivo .cvs con los datos a predecir en batch. Este proyecto lo llamaremos Multiclass_Classification_Batch.
  • Ahora agregamos un proyecto de Machine Learning usando la extensión de Model Builder y seleccionaremos la opción Text Classification. Aunque la opción parezca confusa esta es la opción que podemos usar para cualquier tipo de clasificación binara (binary) o multiclase (Multiclass).
ML.NET Model Builder (Text Classification)
  • Cuando seleccionemos el archivo con el dataset para el entrenamiento del modelo debemos asegurarnos que el archivo contenga una columna id, de esta forma cuando hagamos la predicción en batch podremos identificar sus resultados por el ‘id’ del registro. Sien embargo no queremos que el ‘id’ influya en el entrenamiento por lo que debemos desmarcar esa columna como parte de las características (features). La columna a predecir (label) será la columna reservation_status, esta contiene el estado de la reserva (Booking-Changed; Canceled; Check-Out)
  • Model Builder realizará el entrenamiento del modelo y seleccionará el mejor algoritmo por nosotros, al final se agregará a la solución los proyectos para entrenar y consumir el modelo y nos quedará de esta forma
ML.NET proyectos

Ahora realizaremos las modificaciones al código para hacer una predicción en batch con nuevos registros de reservas de hotel. El primer cambio que debemos hacer es en la clase ModelOutput, esta clase contiene la estructura con el resultado de una predicción, pero como vamos a hacer predicciones en batch vamos a agregar la columna id para poder identificar a cada predicción -por eso es importante que el archivo con los datos de entrenamiento también tengan la columna id-.

public class ModelOutput
{
    // ColumnName attribute is used to change the column name from
    // its default value, which is the name of the field.
    [ColumnName("id")]
    public float Id { get; set; }

    [ColumnName("PredictedLabel")]
    public String Prediction { get; set; }
    public float[] Score { get; set; }
}

Ahora vamos a modificar el archivo ConsumeModel, este archivo contienen el método para hacer predicciones para una nueva reserva de hotel, aquí agregamos un método PredictBatch para hacer las predicciones en batch con nuevas reservas en un archivo .csv y el resultado también será un archivo .csv con las predicciones.

public static string PredictBatch(string dataPath)
{
    MLContext mlContext = new MLContext();

    // Load model
    ITransformer mlModel = mlContext.Model.Load(GetAbsolutePath(MLNetModelPath), out var modelInputSchema);

    IDataView inputData = mlContext.Data.LoadFromTextFile<ModelInput>(dataPath,
                                                                    separatorChar: ',',
                                                                    hasHeader: true);

    var transformedDataView = mlModel.Transform(inputData);

    var predictions = mlContext.Data.CreateEnumerable<ModelOutput>(transformedDataView, false).ToList();

    IDataView outputView = mlContext.Data.LoadFromEnumerable<ModelOutput>(predictions);

    string dataPathOutput = Path.GetDirectoryName(dataPath) + @"\predictions.csv";
    using (FileStream stream = new FileStream(dataPathOutput, FileMode.Create))
    {
        mlContext.Data.SaveAsText(outputView, stream,
                                  separatorChar: ',',
                                  headerRow: true, schema: false);
    }

    return dataPathOutput;
}

Lo interesante de este código es el uso del método LoadFromEnumerable que lo usamos para crear un IDataView con la estructura ModelOutput, lo que demuestra que este método se puede usar con cualquier estructura, no solo con la del ModelInput. Además usamos el método SaveAsText para transformar fácilmente un objeto de tipo List a un archivo .csv sin necesidad de escribir código adicional para este propósito.

Ahora solo nos resta crear una interfaz web en el proyecto Multiclass_Classification_Batch para cargar un archivo con los registros de las reservas de hotel que queremos predecir y descargar el archivo con las predicciones de respuesta. Esto lo haremos directamente en la página Index y agregamos el siguiente código en el HomeController.

[HttpPost]
public async Task<FileResult> Index(IFormFile formFile)
{
    if (formFile != null && formFile.Length > 0)
    {
        string pathImgPredict = Path.Combine(pathPrediction, ConsumeModel.MLNetBatchFileName);
        pathImgPredict = ConsumeModel.GetAbsolutePath(pathImgPredict);
        pathImgPredict = Path.GetFullPath(pathImgPredict);

        using (var stream = System.IO.File.Create(pathImgPredict))
        {
            await formFile.CopyToAsync(stream);
        }

        var predictionsFile = ConsumeModel.PredictBatch(pathImgPredict);

        var streamResult = await System.IO.File.ReadAllBytesAsync(predictionsFile);

        return File(streamResult, "application/csv;charset=utf-8", "Predictions.csv");
    }
    return null;
}

El resultado final de nuestra aplicación será este:

Nótese como en el archivo de respuesta con las predicciones consta la columna Id para poder identificar a qué reserva corresponde cada predicción.

ML CSV

El proyecto completo se puede descargar del repositorio de GitHub

Artículos relacionados

Machine Learning | Trabajar con múltiples fuentes de datos usando ML.NET

Machine Learning | 5 ventajas de la programación orientada a objetos para la ingeniería de datos

¿Por qué las empresas deberían tener un Agile Coach? (y no un consultor de proyectos)

La implementación de metodologías ágiles como Scrum es fundamental para la transformación digital de las empresas, y el rol del Agile Coach se está convirtiendo en uno de los más demandadas rápidamente. En las empresas tradicionales este es el rol del consultor o asesor de proyectos, pero ¿qué es un Agile Coach? y ¿cómo puede hacer que la empresa y sus trabajadores lleguen a conseguir sus objetivos marcados?

¿Qué es un Agile Coach?

Un agile coach es un experto en agilidad, no solo en Scrum o una sola metodología, tiene las competencias y aptitudes necesarias para ayudar a las empresas hacia una transformación agile en sus estructuras organizativas, procesos y herramientas de trabajo. Así como guiar a las personas en la adopción de la mentalidad agile, lo que le permite comprender los principios y valores ágiles. Además, promueve la auto organización de los equipos, autonomía y responsabilidad en el proyecto.

Un agile coach está muy enfocado a la gente -sobre los números-. Su labor es crítica en guiar a los equipos de trabajo en el camino del agilismo, por lo que la experiencia técnica no es suficiente, también se requiere las aptitudes y actitudes de un coach profesional tales como como facilitador, formador, mentor y coach. En definitiva, pretende que las personas den lo mejor de sí mismos.

¿Cuáles son las habilidades de un Agile Coach?

Las principales habilidades de un Agile Coach se resumen en la siguiente infografía

Agile Coach infografia
¿Por qué es necesario un Agile Coach?

Transformar una empresa en ágil, es una labor difícil que implica cambios de procesos, mentalidad y cultura.  Se requiere una visión global de la empresa que le permita ayudar al desarrollo y cambio profesional de sus empleados. Un Agile Coach, localizará los problemas, y notificará a los equipos sobre ellos, de tal forma que sean ellos quienes establezcan la mejor estrategia para solucionarlos.

Artículos relacionados

¿Cómo Scrum puede ayudarnos a gestionar los nuevos equipos remotos?

Machine Learning | Cómo generar nuevos datasets usando bootstrap y ML.NET

Bootstrap es una técnica de remuestreo utilizada para estimar estadísticas de una población mediante el muestreo de un conjunto de datos con reemplazo. En palabras más simples; imaginemos que tenemos una muestra de datos en un dataset y queremos generar una nueva muestra de igual o menor tamaño, entonces tomamos un dato del dataset original de manera aleatoria y la colocamos en un nuevo dataset, a la vez que devolvemos el dato seleccionado al dataset original, y repetimos el proceso hasta generar la nueva muestra deseada. Este enfoque de muestreo se llama muestreo con reemplazo (sampling with replacement).

Fuente: Bootstrap aggregating bagging

Lo interesante de esta técnica es que permite que un dato sea parte de una nueva muestra más de una vez o ni una sola vez. Esto tiene algunas ventajas estadísticas para que las nuevas muestras generadas tengan una mejor distribución que las muestra original.

Esta técnica de bootstrap puede ser muy útil en los siguientes casos.

Cuando se quiere mejorar la distribución de los datos. En el caso de ML.NET la técnica de bootstrap utiliza una distribución de Poisson(1) para generar el nuevo dataset.

Cuando se tiene un dataset muy pequeño y se necesitan más muestras. Ya que los nuevos dataset que genera el remuestreo con bootstrap tiene una valides estadística, estos se pueden usar para entrenar varios modelos con los nuevos dataset o inclusive combinarlos para generar un nuevo dataset con un mayor número de datos.

Para probar un modelo con los datos “fuera de la bolsa” (out-of-bag). Ya que un dato dentro de un dataset puede no ser seleccionado para formar parte del remuestreo, estos datos no seleccionados se los puede usar para generar un nuevo dataset llamado “out-of-bag¨” y usarlo como dataset de prueba para evaluar un modelo generado con un dataset de entrenamiento.

Bootstrap con ML.NET

El código de ejemplo del uso de bootstrap con ml.net, al igual que otros ejemplos está disponible en GitHub.

Primero, y como siempre, iniciamos nuestro objeto mlContext, en este caso no especificamos un seed ya que variaremos ese dato en el método BootstrapSample. Luego cargaremos un dataset con 1000 registros desde un archivo de texto, en este caso son datos de reservas de hotel y su precio.

MLContext mlContext = new MLContext();

            //Cargamos el dataset original
            string dataPath = GetAbsolutePath("../../../data/HotelBookingsRate.tsv");
            var data = mlContext.Data.LoadFromTextFile<ModelInput>(dataPath,
                                                    separatorChar: '\t', hasHeader: true);

En el primer ejemplo crearemos dos remuestreos (bags), cada uno con una semilla diferente (seed), y veremos los resultados en consola.

//Creamos dos remuestreos (bags)
            for (int i = 0; i < 2; i++)
            {
                //generamos el nuevo dataset usando Bootstrap
                var resample = mlContext.Data.BootstrapSample(data, seed: i);

                var enumerable = mlContext.Data
                    .CreateEnumerable<ModelInput>(resample, reuseRowObject: false);

                Console.WriteLine($"Print sample preview out of {enumerable.Count()}");
                PrintHeaders(typeof(ModelInput));

                //ver los datos del nuevo dataset
                //estos dataset los podemos usar para entrenar un modelo
                foreach (var row in enumerable.Take(10))
                {
                    PrintValues(row);
                }
                Console.WriteLine();
            }

En el segundo ejemplo vemos como crear un dataset “out-of-bag” con los datos no seleccionados en el remuestreo, esta técnica nos sirve para usar este dataset como dataset de pruebas para evaluar un modelo generado, nótese en el resultado que el tamaño de este dataset es mucho menor.

//Creamos un remuestreo y un dataset out-of-bag
            //Este dataset lo podemos usar para probar o evaluar un modelo
            var resampleOob = mlContext.Data.BootstrapSample(data, 1, complement: true);

            var enumerableOob = mlContext.Data
                    .CreateEnumerable<ModelInput>(resampleOob, reuseRowObject: false);

            Console.WriteLine($"Print sample preview out of {enumerableOob.Count()}");
            PrintHeaders(typeof(ModelInput));
            foreach (var row in enumerableOob.Take(10))
            {
                PrintValues(row);
            }
Artículos relacionados

Machine Learning | Cómo predecir más de una categoría usando ML.NET

Machine Learning | Trabajar con múltiples fuentes de datos usando ML.NET

Machine Learning | Trabajar con múltiples fuentes de datos usando ML.NET

Cuando desarrollamos modelos de machine learning normalmente debemos trabajar unificando datos de diferentes fuentes como archivos .csv, bases de datos o integraciones con APIs. Una de las ventajas de la programación orientada a objetos es que podemos desarrollar funciones para recuperar datos de múltiples fuentes e instanciarlos en un mismo objeto, sin depender de la compatibilidad de los paquetes que utilicemos. En este post veremos cómo instanciar el objeto de origen de datos (dataset) desde múltiples fuentes usando el framework ML.NET.

En la arquitectura del desarrollo de un modelo de machine learning con ML.NET debemos siempre tener un objeto que represente el origen de datos (dataset) y que por convención lo llamamos ModelInput. Este objeto se lo puede instanciar con los datos de entrenamiento, pruebas o inferencia.

Usando ML.NET podemos cargar los datos para instanciar el objeto ModelInput desde un archivo .csv, desde una tabla en SQL Server, o desde cualquier otra fuente usando un objeto Enumerable. Veamos un ejemplo de una función que utiliza estas tres funcionalidades para cargar datos para el entrenamiento de un modelo.

private static IDataView LoadDataView(string source)
{
    IDataView trainingDataView;

    if (source == "textFile")
    {
        var tmpPath = GetAbsolutePath(TRAIN_DATA_FILEPATH);
        trainingDataView = mlContext.Data.LoadFromTextFile<ModelInput>(
                                    path: tmpPath, hasHeader: true,
                                    separatorChar: '\t', allowQuoting: true,
                                    allowSparse: false);
    }
    else if (source == "SQL")
    {
        DatabaseLoader loader = mlContext.Data.CreateDatabaseLoader<ModelInput>();
        DatabaseSource dbSource = new DatabaseSource(SqlClientFactory.Instance, CONNECTION_STRING, SQL_COMMAND);
        trainingDataView = loader.Load(dbSource);
    }
    else
    {
        //Load form a json file or a rest API
        var data = File.ReadAllText("json1.json");
        var inMemoryCollection = JsonConvert.DeserializeObject<IEnumerable<ModelInput>>(data);

        trainingDataView = mlContext.Data.LoadFromEnumerable<ModelInput>(inMemoryCollection);
    }

    return trainingDataView;
}

En el código anterior se observa las tres funciones que tiene el objeto mlContext.Data para cargar datos al ModelInput

  • LoadFromTextFile() Carga datos desde cualquier archivo .csv o .tsv
  • CreateDatabaseLoader() Carga datos desde una base SQL Server o Azure SQL
  • LoadFromEnumerable() Carga datos desde cualquier lista, se puede usar para cargar datos desde una API o desde un archivo json

Con esta sencilla función podemos cargar datos tanto para el entrenamiento de un modelo o para realizar predicciones de un modelo ya entrenado.

Artículos relacionados: Cómo predecir más de una categoría usando ML.NET

Artículos relacionados: Segmentación de clientes usando ML.NET

Machine Learning | 5 ventajas de la programación orientada a objetos para la ingeniería de datos

Para quienes hemos trabajado toda nuestra vida con lenguajes orientados a objetos como Java, C# o Ruby, cuando incursionamos en el mundo de la ciencia de datos nos puede resultar muy complicado el adaptarnos a lenguajes como Python que es muy popular en este campo debido a la facilidad de aprendizaje para quienes no tienen bases de programación, pero para quienes ya tenemos fuertes conocimientos de desarrollo el usar un lenguaje de programación orientado a objetos también puede tener muchas ventajas para usarlo en la ciencia e ingeniería de datos.

A continuación veremos 5 ventajas de la POO para la ciencia e ingeniería de datos

1 Evitar duplicidad de código. La POO nos permite -y obliga- a encapsular una funcionalidad dentro de un objeto que es descrito con sus propiedades y funciones. Esto ayuda a estandarizar el desarrollo y que los programadores sepan en donde ubicar sus funciones de forma ordenada y lógica, evitando que cada programador agregue funciones específicas para sus necesidades en lugar de diseñarlas de manera reusable y dentro de un contexto adecuado. Veamos un ejemplo de una función completa para crear un modelo de clasificación binaria pero muy simple gracias al uso de funciones encapsuladas y en contexto con la clase creada.

public static class ModelBuilder
{
    // Create MLContext to be shared across the model creation workflow objects 
    // Set a random seed for repeatable/deterministic results across multiple trainings.
    private static MLContext mlContext = new MLContext(seed: 1);

    public static void CreateModel()
    {
        var tmpPath = GetAbsolutePath(TRAIN_DATA_FILEPATH);
        // Load Data
        IDataView trainingDataView = mlContext.Data.LoadFromTextFile<ModelInput>(
                                        path: tmpPath, hasHeader: true,
                                        separatorChar: '\t', allowQuoting: true,
                                        allowSparse: false);


        // Build training pipeline
        IEstimator<ITransformer> trainingPipeline = BuildTrainingPipeline(mlContext);

        // Evaluate quality of Model
        Evaluate(mlContext, trainingDataView, trainingPipeline);

        // Train Model
        ITransformer mlModel = TrainModel(mlContext, trainingDataView, trainingPipeline);

        tmpPath = GetAbsolutePath(MODEL_FILEPATH);
        // Save model
        SaveModel(mlContext, mlModel, tmpPath, trainingDataView.Schema);
    }
}

2 Trabajar con diferentes fuentes de datos. Ya sea para realizar una exploración de datos o para construir un modelo de Machine Learning, siempre vamos a depender de un origen de datos al que debemos acceder. En Python es muy común la práctica de trabajar con archivos .csv y copiar el archivo en cada proyecto para poder acceder a él. Cuando trabajamos con objetos tenemos la ventaja de que al objeto lo podemos instancias de diferentes formas sin importar su origen o sin depender de una librería específica para cada origen. Veamos un ejemplo para instanciar un objeto en una sola función para tres tipos de orígenes diferentes.

private static IDataView LoadDataView(string source)
{
    IDataView trainingDataView;

    if (source == "textFile")
    {
        var tmpPath = GetAbsolutePath(TRAIN_DATA_FILEPATH);
        trainingDataView = mlContext.Data.LoadFromTextFile<ModelInput>(
                                    path: tmpPath, hasHeader: true,
                                    separatorChar: '\t', allowQuoting: true,
                                    allowSparse: false);
    }
    else if (source == "SQL")
    {
        DatabaseLoader loader = mlContext.Data.CreateDatabaseLoader<ModelInput>();
        DatabaseSource dbSource = new DatabaseSource(SqlClientFactory.Instance, CONNECTION_STRING, SQL_COMMAND);
        trainingDataView = loader.Load(dbSource);
    }
    else
    {
        //Load form a json file or a rest API
        var data = File.ReadAllText("json1.json");
        var inMemoryCollection = JsonConvert.DeserializeObject<IEnumerable<ModelInput>>(data);

        trainingDataView = mlContext.Data.LoadFromEnumerable<ModelInput>(inMemoryCollection);
    }

    return trainingDataView;
}

3 Modelos fuertemente tipados. Aunque la posibilidad de no declarar variables o objetos y no especificar su tipo puede ser muy atractivo cuando recién se aprende un nuevo lenguaje, lo cierto es que en grandes proyectos esto puede causar mucha confusión sobretodo cuando el mantenimiento de un código lo tiene que hacer un desarrollador diferente al que lo creó. En la ciencia de datos este puede ser un problema en particular cuando se trabaja con diferentes tipos de modelos, en un lenguaje orientado a objetos podemos usar objetos genéricos para su creación pero instanciarlos fuertemente tipado para explorar con precisión sus características como métricas y parámetros. Veamos un ejemplo de código para instanciar un modelo multiclase.

public static IEstimator<ITransformer> BuildTrainingPipeline(MLContext mlContext)
{
    // Data process configuration with pipeline data transformations 
    var dataProcessPipeline = mlContext.Transforms.Conversion.MapValueToKey("reservation_status", "reservation_status")
                              .Append(mlContext.Transforms.Categorical.OneHotEncoding(new[] { new InputOutputColumnPair("hotel", "hotel"), new InputOutputColumnPair("arrival_date_month", "arrival_date_month"), new InputOutputColumnPair("meal", "meal"), new InputOutputColumnPair("country", "country") }))
                              .Append(mlContext.Transforms.Concatenate("Features", new[] { "hotel", "arrival_date_month", "meal", "country" }));
    // Set the training algorithm
    var trainer = mlContext.MulticlassClassification.Trainers.LightGbm(new LightGbmMulticlassTrainer.Options() { NumberOfIterations = 200, LearningRate = 0.2938725f, NumberOfLeaves = 60, MinimumExampleCountPerLeaf = 10, UseCategoricalSplit = true, HandleMissingValue = false, MinimumExampleCountPerGroup = 10, MaximumCategoricalSplitPointCount = 32, CategoricalSmoothing = 20, L2CategoricalRegularization = 0.5, UseSoftmax = false, Booster = new GradientBooster.Options() { L2Regularization = 0.5, L1Regularization = 1 }, LabelColumnName = "reservation_status", FeatureColumnName = "Features" })
                              .Append(mlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel", "PredictedLabel"));

    var trainingPipeline = dataProcessPipeline.Append(trainer);

    return trainingPipeline;
}

4 Fácil integración con otros sistemas. Los lenguajes como C# tienen frameworks de desarrollo muy completos como .NetFramework o .NetCore que eliminan la dependencia de diferentes paquetes de diferentes creadores para las funciones más básicas, esto mejora sustancialmente la compatibilidad cuando se quiere integrar modelos y predicciones de Machine Learning con las aplicaciones de negocio o APIs de terceros. Veamos un ejemplo de un código para predecir un resultado utilizando un modelo de Machine Learning.

public static ModelOutput Predict(ModelInput input)
{
    // Create new MLContext
    MLContext mlContext = new MLContext();

    // Load model & create prediction engine
    string modelPath = @"MLModel.zip";
    ITransformer mlModel = mlContext.Model.Load(modelPath, out var modelInputSchema);
    var predEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(mlModel);

    // Use model to make prediction on input data
    ModelOutput result = predEngine.Predict(input);
    return result;
}

5 Establecer estándares de trabajo. Finalmente la programación orientada a objetos nos permite desarrollar siguiendo patrones de diseño y de trabajo como TDD, Dependency Injection, entre otros, lo cual facilita mucho el mantenimiento del código y mejora su calidad, sobre todo cuando los proyectos crecen y se tiene a varios desarrolladores compartiendo y desarrollando una misma funcionalidad.

Artículos relacionados: Machine Learning | Cómo predecir más de una categoría usando ML.NET

Machine Learning | Cómo predecir más de una categoría usando ML.NET

Una de las ventajas de usar el framework ML.NET para la ciencia e ingeniería de datos es la posibilidad de crear experimentos para la creación de modelos de Machine Learning de manera automatizada. En este post vamos a usar la clase MulticlassClassificationExperiment incluida en el paquete Microsoft.ML.AutoML para crear un modelo para predecir la probabilidad de cancelación o cambio en la reserva de un hotel.

El código fuente del proyecto se puede descargar de este repositorio en GitHub

Problema

Tenemos un dataset con el histórico de las reservas de varios hoteles y el registro de los cambios que tuvo la reserva. Con estos datos vamos a crear un modelo de Machine Learning para predecir si una reserva puede ser cancelada, modificada o no tener cambios. Este escenario puede ser útil para ofrecer un seguro de cancelación de reserva a un menor o mayor costo según el resultado de la predicción.

Dataset

Contamos con un Dataset tomado de kaggle y modificado para marcar tres categorías en la columna de reservation_status, esta puede tener los valores: Booking-Changed; Canceled; Check-Out

Solución

Crearemos un modelo de Machine Learning usando el framework ML.NET. Ya que son tres categorías las que queremos predecir (Booking-Changed; Canceled; Check-Out) debemos crear un modelo de clasificación multiclase (multiclass classification). Lo haremos de forma automática para que el framework seleccione el mejor algoritmo y los mejores parámetros para nuestro dataset, no tendremos que preocuparnos por la parte matemática, estadística o el tipo de algoritmo a utilizar.

Ya que nuestro modelo debe ser de clasificación multiclase (multiclass classification) usaremos el método CreateMulticlassClassificationExperiment para crear el modelo de forma automática.

Como en toda solución de Machine Learning seguiremos los siguientes pasos para crear el modelo.

  • Preparar y cargar los datos de entrenamiento y prueba
  • Entrenar el modelo para le predicción de los valores deseados
  • Evaluar la precisión del modelo
  • Consumir el modelo desde una aplicación

Los datos los cargaremos en un objeto IDataView desde un archivo de texto con el siguiente código

// Load Data
var tmpPath = GetAbsolutePath(TRAIN_DATA_FILEPATH);
IDataView trainingDataView = mlContext.Data.LoadFromTextFile<ModelInput>(
                                path: tmpPath,
                                hasHeader: true,
                                separatorChar: '\t',
                                allowQuoting: true,
                                allowSparse: false);

var splitData = mlContext.Data
    .TrainTestSplit(trainingDataView, testFraction: 0.15);

El método mlContext.Data.TrainTestSplit() nos sirve para tomar una muestra de nuestro dataset que será usado para probar el modelo creado

Para entrenar el modelo usaremos el método CreateMulticlassClassificationExperiment, al invocarlo necesitamos especificar el tiempo en segundos que queremos que dure el experimento, dependiendo del tamaño de nuestro dataset mientras más tiempo dure el entrenamiento más algoritmos serán probados hasta encontrar el de mejor precisión. El código para crear el experimento es el siguiente.

// STEP 2: Run AutoML experiment
Console.WriteLine($"Running AutoML Multiclass classification experiment for {ExperimentTime} seconds...");
ExperimentResult<MulticlassClassificationMetrics> experimentResult = mlContext.Auto()
         .CreateMulticlassClassificationExperiment(ExperimentTime)                .Execute(trainingDataView, labelColumnName: "reservation_status");

Para usar este método dentro del objeto de contexto mlContext debemos instalar el paquete nuget Microsoft.ML.Auto

using Microsoft.ML.AutoML;

Hasta ahí ese es todo el código que necesitamos para crear nuestro modelo de clasificación multiclase. Pero antes de poder usarlo debemos conocer sus métricas y su precisión para hacer predicciones. Lo primero que podemos observar es el algoritmo seleccionado para el modelo y las métricas que lo calificaron como el mejor algoritmo para nuestros datos. Esto lo podemos hacer con este código

// STEP 3: Print metric from the best model
RunDetail<MulticlassClassificationMetrics> bestRun = experimentResult.BestRun;
Console.WriteLine($"Total models produced: {experimentResult.RunDetails.Count()}");
Console.WriteLine($"Best model's trainer: {bestRun.TrainerName}");
Console.WriteLine($"Metrics of best model from validation data --");
PrintMulticlassClassificationMetrics(bestRun.ValidationMetrics);

El siguiente paso es probar el modelo con datos de prueba no incluidos en el entrenamiento y ver si las métricas son similares a las métricas del entrenamiento del modelo.

// STEP 4: Evaluate test data
IDataView testDataViewWithBestScore = bestRun.Model.Transform(splitData.TestSet);
var testMetrics = mlContext.MulticlassClassification.CrossValidate(testDataViewWithBestScore, bestRun.Estimator, numberOfFolds: 5, labelColumnName: "reservation_status");
Console.WriteLine($"Metrics of best model on test data --");
PrintMulticlassClassificationFoldsAverageMetrics(testMetrics);

Los resultados de este experimento en resumen son los siguientes

  • Mejor algoritmo seleccionado: LightGbmMulti. Una de las ventajas de generar un modelo de forma automática es que la selección del algoritmo no está sesgada por la preferencia del data scientist, durante el experimento se han probado varios algoritmos y configuraciones
  • Precisión del modelo: 97%
  • Con los datos de prueba se obtuvo en promedio un 97% de precision en las predicciones lo que nos una confianza bastante buena para poder usar nuestro modelo generado

El siguiente paso es guardar el modelo en un archivo .zip para luego usarlo desde una aplicación

// Save model
SaveModel(mlContext, bestRun.Model, MODEL_FILEPATH, trainingDataView.Schema);

Como último paso crearemos una clase ConsumeModel y un método Predict() para consumir el modelo y probar una predicción con los datos de una nueva reserva de hotel.

// Create new MLContext
MLContext mlContext = new MLContext();

// Load model & create prediction engine
string modelPath = @"MLModel.zip";
ITransformer mlModel = mlContext.Model.Load(modelPath, out var modelInputSchema);
var predEngine = mlContext.Model.CreatePredictionEngine<ModelInput, ModelOutput>(mlModel, modelInputSchema);

// Use model to make prediction on input data
ModelOutput result = predEngine.Predict(input);
return result;

Una recomendación de diseño es crear las clases ConsumeModel, ModelInput y ModelOutput en un solo proyecto, puede ser el mismo en el que creamos y entrenamos el modelo, de esa forma, En la aplicación para consumir el modelo y hacer una predicción con nuevos datos solo deberemos referencias el proyecto con la clase ConsumeModel y no será necesario instalar los paquetes de Microsoft.ML.

El código en la aplicación para predecir el cambio de estado en una reserva de hotel se verá así.

Console.WriteLine("Tiempo de anticipacion: ");
string leadTime = Console.ReadLine();
Console.WriteLine("Cuarto reservado: ");
string cuartoReservado = Console.ReadLine();
Console.WriteLine("Precio promedio: ");
string adr = Console.ReadLine();
Console.WriteLine("Pais: ");
string country = Console.ReadLine();
Console.WriteLine("Cambios previos: ");
string cambiosPrevios = Console.ReadLine();


ModelInput bookingData = CreateBookingData(leadTime, cuartoReservado, cambiosPrevios, adr, country);

ModelOutput bookingPredict = PredecirCambioReserva(bookingData);

Console.WriteLine($"\n\nActual Reservation_status: {bookingData.Reservation_status} \nPredicted Reservation_status value {bookingPredict.Prediction} \nPredicted Reservation_status scores: [{String.Join(",", bookingPredict.Score)}]\n\n");

En este caso simulamos la entrada de solo cinco variables de todo el dataset, realizamos la predicción y además vemos la métrica de cada categoría.

El código fuente del proyecto se puede descargar de este repositorio en GitHub

Artículos Relacionados

Segmentación de clientes usando ML.NET

¿Cómo Scrum puede ayudarnos a gestionar los nuevos equipos remotos?

La gestión de equipos remotos requiere las mismas habilidades e instrucciones que cualquier equipo presencial: Objetivos claros, retroalimentación, interacción, enfoque. Pero la distancia añade retos adicionales, la adopción de un marco de trabajo ágil como Scrum puede ayudarnos a superar estos retos.

En el mercado existen muchas herramientas tecnológicas que nos ayudan a mantenernos conectados y ¨”cercanos” y son altamente recomendadas como Microsoft Teams, Google Meet, Slack y por supuesto la estrella de las reuniones Zoom. Pero estas herramientas no nos ayudan a superar ciertos retos culturales y relacionadas al comportamiento humano. Veamos algunos de estos retos que se presentan en equipos remotos.

1 Perdida de identidad con la compañía. Este es un problema crítico y crece cada día sin que nos demos cuenta. Los humanos tendemos a socializar en grupo. En la oficina se forman amistades entre las personas. Todos son parte de un mismo equipo en la compañía. Cuando la gente trabaja remotamente, y ya nos e comparte un café o un desayuno, se forma una sensación extraña. Se pierden los nexos y conexiones con las personas que ya no conocemos. La consecuencia es que la compañía se divide en grupos y estos grupos van a tener mayor dificultad para trabajar en conjunto.

2 El conocimiento no llega a los equipos. Aún en equipos que han trabajado juntos por muchos años, enfrentan retos en la transferencia de conocimiento. En muchos casos, el equipo de desarrollo está alejado del product owner. Además, el equipo de desarrollo está alejado de los usuarios del producto que están creando y dependen del product owner para entender los requerimientos.

3 Pérdida de comunicación. Cuando un equipo trabaja de forma presencial, conversa. almuerzan, se toman un café juntos, y las reuniones son más frecuentes. pero en un equipo remoto esto no sucede. El problema es que un equipo remoto necesita aún más comunicación. La comunicación es clave para superar los retos que hemos visto. Así que tenemos que encontrar los mecanismos para aumentar la comunicación en equipos remotos.

4 La caja negra. Cuando el equipo remoto está alejado, no vemos lo que está haciendo ni quién está trabajando en nuestro producto. El equipo remoto se convierte en una caja negra, Enviamos requerimientos y esperamos una salida. En este entorno se vuelve más necesario la adopción del agilismo. Los miembros del equipo remoto tienen que ser vistos como un solo equipo, tenemos que saber cómo cada miembro aporta al equipo, la caja negra tiene que ser abierta

Ya conocemos los retos, ahora respondamos la pregunta ¿Cómo Scrum puede ayudarnos a gestionar los nuevos equipos remotos?

El Scrum Master toma más importancia en una organización.

El Scrum Master es el rol por excelencia para remover los impedimentos del equipo y hay algunas recomendaciones que puede seguir para mantener al equipo comunicado y unido.

Adaptar y mantener una disciplina en el uso de herramientas. Pasar de las herramientas físicas como paneles Kanban y las reuniones diarias presenciales a herramientas digitales y reuniones virtuales tomará tiempo de adaptación. Para esto el Scrum Master deberá generar el hábito de uso de las herramientas para el trabajo remoto.

Sprints más cortos para mantener los objetivos simples. Sprints y retrospectivas cada semana pueden ser más efectivos en equipos remotos para no descuidar el enfoque en los objetivos y avances del proyecto.

Reuniones más interactivas y en equipos más pequeños. Es de vital importancia mantener el interés en las reuniones remotas y esto se puede perder si hay muchos participantes, la recomendación es que se armen equipos más pequeños para que todos puedan -y deban- participar activamente en la reunión y que todos aporten algo.

El Product Owner debe conocer las nuevas necesidades de los clientes

En la llamada “nueva normalidad” las necesidades de los clientes no van a ser las mismas. El valor que las empresas y sus servicios aportan a la sociedad van a tomar mucha más importante que antes. El Product Owner debe conocer esas nuevas necesidades para definir con más prioridad los productos que tengan estén alineados con estas nuevas necesidades. Además el Product Owner debe convertirse en un experto en experiencia del cliente ya que en la actualidad esto hace la diferencia entre el éxito o fracaso de un producto.

Estas habilidades deben ser reforzadas en el Product Owner ya que en equipos remotos es quizá el único punto de contacto entre el negocio y el equipo de desarrollo, de esta manera evitamos la dependencia de involucrar a usuarios o más miembros al equipo del proyecto.

El conocimiento tiene que estar más disponible para todos.

El objetivo es asegurarse que todos los miembros del equipo tengan acceso igualitario a información crucial del proyecto, hay que romper el paradigma de “información reservada” o el miedo a que “se van a llevar el conocimiento”.

Un equipo presencial tiene más interacciones con el usuario y Product Owners. EN equipos remotos se necesita un plan para acceder a esa información. Reuniones de planificación más efectivas, más involucramiento del Product Owner en estas reuniones y pensamiento creativo para obtener mejor feedback de los usuarios en las etapas del proyecto.

Fallar rápido y barato

Una de las bases de Scrum y el agilismo es tomar decisiones más rápidas, con la premisa de que demasiado análisis puede ser más costoso que ejecutar una idea aunque esta sea fallida. Esto toma más relevancia en momentos de cambio o de incertidumbre. En tiempos en que muchos negocios tienen que reinventarse o cambiar la propuesta de sus negocios, no podemos pasar demasiado tiempo esperando encontrar la solución perfecta. Un producto más simple con objetivos claros puede determinar el éxito o fracaso en la adaptación de nuestro negocio.

Artículos relacionados: ¿El Coronavirus acelerará la transformación digital de las empresas?

¿El Coronavirus acelerará la transformación digital de las empresas?

La transformación digital ha potenciado las estrategias de negocio de pequeñas, medianas y grandes empresas mediante el uso de plataformas digitales para cumplir con las expectativas de sus clientes.

Las plataformas digitales se encuentran disponibles desde hace varios años pero muchas compañías no las han implementado para transformar sus negocios y encontrar mayores oportunidades para crecer, ser más eficientes y diferenciarse de la competencia.

La llegada del coronavirus ha puesto a prueba a miles de compañías en el mundo que no estaban preparadas para afrontar una pandemia económica y menos para operar en tiempos de confinamiento preventivo decretado por muchos países en el mundo, poniendo en jaque la economía mundial y por ende a muchas compañías que no han empezado a implementar la transformación digital.

Ahora los negocios están obligadas a transformarse y estas son 4 razones por las que el Coronavirus acelerará la transformación digital en las empresas

1. El teletrabajo se volverá parte de la cultura empresarial.

Según Forbes el 29% de los trabajadores remotos dicen que son más felices que los trabajadores que deben estar en un lugar fijo.

El teletrabajo puede ser mucho más productivo que el trabajo presencial si se lo realiza con las herramientas y el liderazgo adecuado. En teletrabajo pierde sentido la evaluación por tiempo de trabajo y gana prioridad la evaluación por objetivos y resultados. Es verdad que no todas las funciones se pueden adecuar al trabajo remoto de la misma forma, pero aquellas que funciones que se pueden cumplir de forma remota sin impacto al cliente deberán seguir siendo impulsadas por las empresas aún cuando se levanten las medidas de confinamiento.

2. Las empresas adoptarán las tecnologías en la nube como estrategia para adaptarse a los cambios.

Migrar los procesos a servicios en la nube como AWS o Microsoft Azure permite a las empresas usar herramientas tecnológicas más actualizadas, seguras y flexibles a los cambios, sin embargo los líderes de las compañías deben asegurarse que esta transición sea de manera gradual y bien estratégica donde puedan garantizar que los empleados y el equipo de la compañía en general tenga todos los conocimientos para empezar a operar de una manera más digital.

Los servicios en la nube también ayudan a que las empresas adopten el teletrabajo de forma más rápida. Empresas como Google, Microsoft y Zoom están ofreciendo funciones gratuitas para sus herramientas de comunicación y colaboración, lo que permite crear una política de trabajo remoto para llevar los registros diarios, trabajos basados en tareas y diferentes planes individuales para que las personas se adapten a esta forma de hacer teletrabajo.

3. El e-commerce está cambiando el paradigma de la interacción entre el cliente y la empresa.

Según Marc Benioff, CEO de Salesforce, “cada transformación digital comenzará y terminará con el cliente”, es así como muchas nuevas empresas están adoptando modelos similares, encontrando en el e-commerce y las plataformas digitales una nueva forma de hacer negocios de manera más remota, eficaz, reduce los costos y mejora la experiencia de los clientes.

El e-commerce también trae el reto a las empresas de conocer más a sus clientes para brindarle servicios y experiencias personalizadas, utilizar modelos de machine learning para recomendaciones y digitalizar todos los puntos de interacción con el cliente tanto en la preventa como en postventa.

4. El agilismo será la principal forma de trabajo para desarrollar productos.

Muchos expertos coinciden en que luego de la pandemia enfrentaremos una “nueva normalidad” pero todavía el panorama es muy incierto. Las empresas deben estar mejor preparadas para adaptarse a los cambios de forma rápida y responder a las nuevas necesidades de los clientes. Los marcos de trabajo ágiles como Scrum deberán ser adoptados por todas las áreas de la empresa, ya no será una característica solo de las áreas de tecnología. La colaboración y el trabajo en equipos multidisciplinarios harán la diferencia entre las empresas que puedan adaptarse a diferentes modelos de negocio y desarrollar productos adecuados a la “nueva normalidad”

Artículo relacionado: Scrum explicado con Masterchef