Análisis de Pérdida de clientes: Base de SQL

Este posteo explica el primer demo de la presentación de análisis de abandono de clientes. La intención de que puedas replicarla o adaptarla a tu ambiente. El objetivo final de la presentación es buscar el análisis de la pérdida de clientes antes de crear los modelos predictivos.  El abandono o pérdida de clientes lo llamamos también en inglés “Customer Churn”.

Para crear el modelo de abandono de clientes, inicie con un modelo lo más modesto posible y que permita enforcarse en el problema que queremos resolver y no distraiga demasiado. Hay algunos detalles sin embargo que no deben pasar desapercibidos:

  • Las tablas de transacciones (en este caso las ventas) son tablas delgadas y altas. O sea, de pocas columnas y muchas filas.
  • Las tablas de catalogo son tablas gruesas y bajas. O sea, de muchas columnas y generalmente pocas filas (aunque no siempre).

Este patrón (basado en el modelo dimensional de Kimball) hace modelos de Inteligencia de negocios que tienen mejor desempeño y más “inteligentes”. Modelos que tienen 3 o 4 atributos en las dimensiones o catálogos son modelos de bajo IQ (Coeficiente Intelectual) y por ende no son muy útiles.

Diagrama del Modelo de Datos

Este diagrama es OPTATIVO, se usa solo si quieres crear una representación gráfica de las entidades. Para diagramar use https://dbdiagram.io/. Se usa el siguiente pseudo código, que crea tablas, pero en realidad son vistas.

//// -- LEVEL 1
//// -- Tables and References
// Creating tables
 Table Sales {
    SalesOrderID int [pk, increment] 
    OrderDate date 
    DueDate date 
    ShipDate date 
    CustomerID int 
    SalesPersonID int
    TerritoryID int 
    BillToAddressID int 
    ShipToAddressID int 
    ShipMethodID int 
    CreditCardID int 
    SubTotal money 
    TaxAmt money 
    Freight money 
    TotalDue money
}
Table Stores {
    CustomerID int  [pk, increment] 
    Name VARCHAR(30) 
    AddressType VARCHAR(30)  
    AddressLine1 VARCHAR(60) 
    AddressLine2 VARCHAR(60) 
    City VARCHAR(30) 
    StateProvinceName VARCHAR(30) 
    PostalCode VARCHAR(15) 
    CountryRegionName VARCHAR 
}
Table Individuals{
    CustomerID int [pk, increment] 
    FullName nvarchar(152) 
    AddressType varchar(30) 
    AddressLine1 nvarchar(60) 
    AddressLine2 nvarchar(60) 
    City nvarchar(30) 
    StateProvinceName varchar(30) 
    PostalCode nvarchar(15) 
    CountryRegionName varchar(30) 
    EmailPromotion int 
    Occupation varchar(50) 
    Education varchar(50) 
    HomeOwnerFlag bit 
    NumberCarsOwned int 
    Gender char(1) 
    MaritalStatus char(1) 
    BirthDate date 
    DateFirstPurchase date 
}
TABLE Dates{
    DateId int 
    Date date  [pk]
    DateFullName varchar(15) 
    DayNumberOfMonth int 
    DayNumberOfQuarter int 
    DayNumberOfYear int 
    MonthKey smallint 
    MonthName varchar(10) 
    MonthFullKey int 
    MonthFullName varchar(15) 
    MonthNumberOfQuarter int 
    MonthNumberOfYear int 
    MonthDaysInMonth int 
    MonthBeginDate varchar(15) 
    MonthEndDate varchar(15) 
    QuarterKey smallint 
    QuarterName char(2) 
    QuarterFullKey int 
    QuarterFullName char(7) 
    QuarterNumberOfDays int 
    QuarterBeginDate varchar(15) 
    QuarterEndDate varchar(15) 
    YearKey smallint 
    YearName varchar(4) 
    YearNumberOfDays int 
    YearBeginDate varchar(15) 
    YearEndDate varchar(15) 
    DayKey smallint 
    DayName varchar(10) 
    WeekKey smallint 
    WeekName char(3) 
    WeekFullKey int 
    WeekFullName char(8) 
    WeekBeginDate varchar(15) 
    WeekEndDate varchar(15) 
    FiscalMonthFullName varchar(20) 
    FiscalQuarterFullName varchar(12) 
    FiscalYearKey smallint 
    FiscalYearName varchar(9) 
    FiscalYearBeginDate varchar(15) 
    FiscalYearEndDate varchar(15) 
    DateTypeKey smallint 
    DateTypeName varchar(12) 
    HolidayKey int 
    HolidayName varchar(40) 
    WeatherSeasonKey smallint 
    WeatherSeasonName varchar(20) 
    WeatherSeasonFullKey int 
    WeatherSeasonFullName varchar(20) 
    WeatherYearKey smallint 
    WeatherYearName varchar(4) 
    BusinessSeasonKey smallint 
    BusinessSeasonName varchar(20) 
    BusinessSeasonFullKey int 
    BusinessSeasonFullName varchar(26) 
    BusinessYearKey smallint 
    BusinessYearName varchar(4) 
}
Ref: Sales.CustomerID > Stores.CustomerID
Ref: Sales.CustomerID > Individuals.CustomerID
Ref: Sales.OrderDate > Dates.Date

Hay que notar la relación de Individuos y Tiendas. En realidad, hay una entidad “escondida” de clientes que no está modelada y que existe entre órdenes y las entidades de Tiendas e individuos. Reitero, que esta entidad NO se modela para reflejar la realidad de lo que nos encontramos en las bases de datos usuales, y es que no siempre los modelos son perfectamente bien modelados. La grafica resultante del modelo es la siguiente:

Las entidades usadas son:

Entidad de Ordenes (Facturas o Albaranes)

En esta consulta solo ajusto las fechas para que queden más cerca del día de hoy. Muchos atributos se pueden quitar, los dejo porque pueden ser interesantes para otras demos.

SELECT SalesOrderID, 
       CAST(DATEADD(MONTH, 6, DATEADD(YEAR, 5, OrderDate)) AS DATE) AS OrderDate, 
       CAST(DATEADD(MONTH, 6, DATEADD(YEAR, 5, DueDate)) AS DATE) AS DueDate, 
       CAST(DATEADD(MONTH, 6, DATEADD(YEAR, 5, ShipDate)) AS DATE) AS ShipDate,
       ---, OnlineOrderFlag, SalesOrderNumber
       --, PurchaseOrderNumber, AccountNumber 
       CustomerID, 
       SalesPersonID, 
       TerritoryID, 
       --BillToAddressID, 
       --ShipToAddressID, 
       --ShipMethodID, 
       --CreditCardID,
       --, CreditCardApprovalCode
       --, CurrencyRateID 
       SubTotal, 
       TaxAmt, 
       Freight, 
       TotalDue
FROM Sales.SalesOrderHeader AS OH;

Resultado de la consulta:

SalesOrderIDOrderDateDueDateShipDateCustomerIDSalesPersonIDTerritoryIDSubTotalTaxAmtFreightTotalDue
436592016-11-302016-12-122016-12-0729825279520565.62061971.5149616.098423153.2339
436602016-11-302016-12-122016-12-072967227951294.2529124.248338.82761457.3288
436612016-11-302016-12-122016-12-0729734282632726.47863153.7696985.553036865.8012
436622016-11-302016-12-122016-12-0729994282628832.52892775.1646867.238932474.9324
436632016-11-302016-12-122016-12-07295652764419.458940.268112.5838472.3108
436642016-11-302016-12-122016-12-0729898280124432.60882344.9921732.810027510.4109
436652016-11-302016-12-122016-12-0729580283114352.77131375.9427429.982116158.6961
436662016-11-302016-12-122016-12-073005227645056.4896486.3747151.99215694.8564
436672016-11-302016-12-122016-12-072997427736107.0820586.1203183.16266876.3649
436682016-11-302016-12-122016-12-0729614282635944.15623461.76541081.801740487.7233

Vista de Tiendas

Tiene muy pocos atributos interesantes, solo los relacionados con geografía. En un ambiente de producción, deberíamos considerar buscar otros atributos relevantes. En esta consulta, el truco más relevante, es usar la función ROW_NUMBER para escoger la dirección preferida, en este caso ordenada de acuerdo con una regla de negocio donde Main Office es preferida, luego Primary y de último Billing. Otra nota importante es asegurarse que la consulta sea determinística, o sea que siempre devuelve el mismo conjunto de datos por eso se usa en el ORDER BY del ROW_NUMBER la columna AddressTypeID para asegurarse que siempre asigna el mismo número a las filas en caso de empate, dicho de otra forma, AddressTypeID sirve para desempatar. Adicionalmente se filtran solo los clientes con ordenes por eso la condición en el WHERE filtra clientes registrados sin órdenes. Finalmente, revise que en parte inferior de la consulta se usa el ROW_NUMBER para que escoger solo una dirección por cliente.

CREATE VIEW Demos.Stores AS 
    WITH BaseStore AS (
        SELECT 
            Cus.CustomerID
            ,s.Name 
            ,at.Name AS AddressType
            ,a.AddressLine1 
            ,a.AddressLine2 
            ,a.City 
            ,sp.Name AS StateProvinceName 
            ,a.PostalCode 
            ,cr.Name AS CountryRegionName 
            , ROW_NUMBER() OVER(PARTITION BY Cus.CustomerID
                ORDER BY CASE at.Name WHEN 'Main Office'  THEN 1    
                                      WHEN 'Primary'  THEN 2      
                                      WHEN 'Billing'  THEN 3    
                                      ELSE 4 END, bea.AddressTypeID ) AS RSel
        FROM Sales.Customer AS Cus
        JOIN Sales.Store AS S
            ON Cus.StoreID=S.BusinessEntityID
        --- Dups
        INNER JOIN Person.BusinessEntityAddress AS bea 
            ON bea.BusinessEntityID = s.BusinessEntityID 
        INNER JOIN Person.Address AS a 
            ON a.AddressID = bea.AddressID
        INNER JOIN Person.StateProvince AS sp 
            ON sp.StateProvinceID = a.StateProvinceID
        INNER JOIN Person.CountryRegion AS cr 
            ON cr.CountryRegionCode = sp.CountryRegionCode
        INNER JOIN Person.AddressType AS at 
            ON at.AddressTypeID = bea.AddressTypeID
        WHERE Cus.StoreID IS NOT NULL
        AND Cus.CustomerID IN (SELECT  oh.CustomerID
            FROM Sales.SalesOrderHeader AS OH)
    )
    SELECT CustomerID, Name, AddressType, AddressLine1, AddressLine2, City, StateProvinceName, PostalCode
        , CountryRegionName 
    FROM BaseStore
    WHERE RSel=1;
SELECT TOP 20 * 
FROM Demos.Stores;

CustomerIDNameAddressTypeAddressLine1AddressLine2CityStateProvinceNamePostalCodeCountryRegionName
29484Next-Door Bike StoreMain OfficeMall Of MemphisNAMemphisTennessee38103United States
29485Professional Sales and ServiceMain Office57251 Serene BlvdNAVan NuysCalifornia91411United States
29486Riders CompanyMain OfficeTanger FactoryNABranchMinnesota55056United States
29487The Bike MechanicsMain OfficeJohnny Appleseed Shop.centerNAMansfieldOhio44903United States
29488Nationwide SupplyMain Office4250 Concord RoadNARhodesNew South Wales2138Australia
29489Area Bike AccessoriesMain Office6900 Sisk RoadNAModestoCalifornia95354United States
29490Bicycle Accessories and KitsMain OfficeLewiston MallNALewistonIdaho83501United States
29491Clamps & Brackets Co.Main OfficeLeesburg Premium Outlet CentreNALeesburgVirginia20176United States
29492Valley Bicycle SpecialistsMain OfficeBlue Ridge MallNAKansas CityMissouri64106United States
29493New Bikes CompanyMain OfficeHilton Head Factory Outlets No. 25NABlufftonSouth Carolina29910United States

Vista de Individuos

La más interesante desde el punto de vista de atributos para análisis. Con la misma técnica de filtros de direcciones. El uso del CROSS APPLY para extraer de la columna XML los datos demográficos y normalizarlos.

CREATE VIEW Demos.Individuals
AS 
    WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/adventure-works/IndividualSurvey' AS sv)
    , BaseIndividuos AS (
    SELECT 
        C.CustomerID,
        RTRIM(
            CONCAT(
                COALESCE(p.FirstName + ' ', '')
                , COALESCE(p.MiddleName + ' ', '')
                , COALESCE(p.LastName, '')
            )) AS FullName
        ,at.Name AS AddressType
        ,a.AddressLine1
        ,a.AddressLine2
        ,a.City
        ,StateProvinceName = sp.Name
        ,a.PostalCode
        ,CountryRegionName = cr.Name
        ,p.EmailPromotion
        , C.value('sv:Occupation[1]','varchar(50)') AS Occupation
        , C.value('sv:Education[1]','varchar(50)') AS Education
        , C.value('sv:HomeOwnerFlag[1]','bit') AS HomeOwnerFlag
        , C.value('sv:NumberCarsOwned[1]','int') AS NumberCarsOwned
        , C.value('sv:Gender[1]','char(1)') AS Gender
        , C.value('sv:MaritalStatus[1]','char(1)') AS MaritalStatus
        , C.value('sv:BirthDate[1]','date') AS BirthDate
        , CAST(DATEADD(MONTH, 6, DATEADD(YEAR, 5, 
            C.value('sv:DateFirstPurchase[1]','date'))) AS DATE) AS DateFirstPurchase
        , ROW_NUMBER() OVER(PARTITION BY C.CustomerID
            ORDER BY CASE at.Name WHEN 'Home'  THEN 1   
                                    ELSE 4 END, bea.AddressTypeID ) AS RSel
    FROM Sales.Customer AS c
    JOIN Person.Person p
        ON c.PersonID = p.BusinessEntityID
        INNER JOIN Person.BusinessEntityAddress bea 
        ON bea.BusinessEntityID = p.BusinessEntityID 
        INNER JOIN Person.Address a 
        ON a.AddressID = bea.AddressID
        INNER JOIN Person.StateProvince sp 
        ON sp.StateProvinceID = a.StateProvinceID
        INNER JOIN Person.CountryRegion cr 
        ON cr.CountryRegionCode = sp.CountryRegionCode
        INNER JOIN Person.AddressType at 
        ON at.AddressTypeID = bea.AddressTypeID
        LEFT OUTER JOIN Person.EmailAddress ea
        ON ea.BusinessEntityID = p.BusinessEntityID
        LEFT OUTER JOIN Person.PersonPhone pp
        ON pp.BusinessEntityID = p.BusinessEntityID
        LEFT OUTER JOIN Person.PhoneNumberType pnt
        ON pnt.PhoneNumberTypeID = pp.PhoneNumberTypeID
    CROSS APPLY p.Demographics.nodes('/sv:IndividualSurvey') AS SV(C)
    WHERE c.PersonID IS NOT NULL
        AND c.CustomerID IN (SELECT  oh.CustomerID
            FROM Sales.SalesOrderHeader AS OH)
    )
    SELECT CustomerID, FullName, AddressType, AddressLine1, AddressLine2, City, StateProvinceName, PostalCode
        , CountryRegionName, EmailPromotion, Occupation, Education, HomeOwnerFlag, NumberCarsOwned, Gender
        , MaritalStatus, BirthDate, DateFirstPurchase
    FROM BaseIndividuos
    WHERE RSel=1;

Tabla de Fechas

La tabla de fechas, pertenece a una demostración más compleja y tiene su propia presentación (Construyendo una Dimensión de Tiempo excepcional) y los detalles técnicos los dejo para ese momento.

Próximamente estaré agregando el resto de las demostraciones. Si te queda alguna duda técnica o si solo quieres comentarme como te fue con la replicación del ejemplo, déjamelo saber en los comentarios.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *