Continuamos con nuestro generador de datos de prueba hecho en python.

Llegados a este punto, como la lógica va a empezar a complicarse, vamos a separar el código de las entidades en distintos archivos .py. Esto nos va a permitir, por un lado, hacer el código más legible y más fácil de mantener, en tanto que un cambio en una entidad quedará aislado en dicha entidad; por el otro desacoplar el código nos permitirá hacerlo más reutilizable. Si queremos una entidad derivada de persona pero con otras propiedades, manteniendo la persona, podremos copiar Person.py a otro archivo, realizar los cambios allí y generar una nueva entidad (por ejemplo, empleado) que el código principal utilice en otro escenario.

Así, nuestro punto de partida es el mismo código de antes, pero separado en varios archivos.

generando datos prueba python person

Nótese cómo en Person.py se ha comentado las filas relativas al historial de ventas. Ahora, desde persona no se ve Operación lo que implica que no podemos dar operaciones a la persona. Cambiaremos la filosofía de la asignación de ventas en el desarrollo de este tutorial

Estrategia

Para simular un escenario real vamos a hacer las siguientes suposiciones:

  • El tiempo pasa: para generar las operaciones, vamos a hacer pasar el tiempo desde el archivo principal (Generator.py) con un bucle que recorra una lista de fechas para cada persona. Así, cualquier persona tendrá cierta probabilidad de realizar una operación en una fecha concreta. Si elaboramos el paso del tiempo con cuidado, podremos establecer estacionalidad en las operaciones (por ejemplo, más compras en diciembre, o menos en agosto)
  • Geografía: vamos a montar una dimensión geográfica nueva, con la estructura País / Ciudad / Tienda. Así, podremos, ademas de dar estacionalidad a las compras, dar sentido geográfico. Por ejemplo: una persona debería comprar más en su ciudad de origen, y un parámetro podría determinar la propensión de dicha persona a viajar, y por lo tanto, comprar fuera de su ciudad.

 

Geografía

Manos a la obra, vamos a generar un nuevo archivo Geography.py para modelar la geografía. Bien podemos, como decíamos antes, sacar un duplicado de persona y vaciarlo, aprovechando así el molde del archivo de entidad.

Nuestro primer modelo de geografía tendrá, en cada ciudad, una lista de tamaño aleatorio de tiendas.

from faker import Faker
import json
import datetime
import random
class City:
def __init__(self,i):
fake = Faker(‘es_ES’)
self.id = i
self.name = fake.city()
self.stores = [Store(i) for i in range(1,random.randint(2,8))]
def get_item(self):
p = {
‘id_ciudad’: self.id,
‘Ciudad’: self.name,
‘Stores’: [s.get_item() for s in self.stores]
}
return (p)
class Store:
def __init__(self,i):
fake = Faker(‘es_ES’)
self.id = i
self.storeName = fake.company()
def get_item(self):
p = {
‘id_store’: self.id,
‘Store’: self.storeName
}
return (p)
 

En primer lugar, notemos que el constructor de la ciudad, así como el de la tienda, contienen un parámetro i que determina el id del elemento. El razonamiento detrás de esto es que la lista ordenada de entidades se genera fuera de la entidad, en Generator.py, por lo que necesitamos ordenar desde fuera los elementos. Este ID se asigna a un atributo y luego se serializa.

Por otro lado, en la construcción de tiendas, vemos que generamos un atributo stores con un array con las tiendas sin serializar, y luego al serializar, montamos un array serializando cada tienda.

Es importante notar que la tienda no tiene ID único, sino que su id dependerá de la ciudad en la que se encuentre, así, cada ciudad tendrá una lista de tiendas cuyos índices comienzan en 1.

Con esta estructura en Geography.py podemos generar una lista de personas y otra de ciudades en Generator.py. Aprovechamos la coyuntura para generalizar la creación de los archivos JSON:

from faker import Faker
import json, datetime, random, jsbeautifier
from Person import Person
from Operation import Operation
from Geography import City
 
def saveJson(entity,filename):
with open(filename,‘w’) as jsonfile:
for e in entity:
jsonfile.write(jsbeautifier.beautify(json.dumps(e.get_item())))
def main():
numPersonas = 2
numCiudades = 2
People = [Person(i) for i in range(1,numPersonas+1)]
Geo = [City(i) for i in range(1,numCiudades+1)]
saveJson(People,‘People.json’)
saveJson(Geo,‘Geo.json’)
main()
 

Y como resultado obtenemos:

generando datos prueba python stores

Y el tiempo pasará...

Como dice la canción.

Tanto para persona como para ciudades, hemos generado una lista, sin más, con tantos elementos como hemos querido. Además, estos elementos se generaban ya serializados con la función get_item() y lo hemos hecho así para poder guardarlos de manera sencilla y ver el resultado.

Ahora bien, el paso del tiempo nos trae otro reto. Primero, necesitamos recorrer una lista de fechas, eso es sencillo. Pero luego, necesitamos una función que reciba los objetos City y Person y que los relacione de alguna manera.

Así, lo que queremos es una entidad Operation que reciba, en su constructor, de qué persona estamos hablando, la lista de ciudades (más adelante veremos por qué) para elegir de allí una ciudad y de la ciudad una tienda; y por último la fecha para la que estamos calculando la operación. Podría ser algo como esto:

from faker import Faker
import json
import datetime
import random
from Person import Person
from Geography import City,Store
class Operation:
def __init__(self,person,date,geo):
fake = Faker(‘es_ES’)
self.client = person
self.city = random.choice(geo)
self.store = random.choice(self.city.stores)
self.valor = round(random.random()*random.random()*10000)
self.date = date
def get_item(self):
p = {
‘Valor’: self.valor,
‘Cliente’: self.client.id ,
‘Ciudad’: self.city.id,
‘Tienda’: self.store.id,
‘Fecha’: self.date.strftime(‘%d/%m/%Y’)
}
return (p)
 
 

De aquí, vemos que nos guardamos el cliente entero y que de la ciudad y de las tiendas de las ciudades elegimos al azar. Sólo anotaremos que para serializar la fecha necesitamos convertirla a string con strftime.

Una vez nuestra entidad tiene los atributos que relacionan las dimensiones, podemos incluir en Generator.py las reglas para construir una lista de operaciones y así, tener una tabla de hechos:

from faker import Faker
import json, datetime, random, jsbeautifier
from Person import Person
from Operation import Operation
from Geography import City
def saveJson(entity,filename):
with open(filename,‘w’) as jsonfile:
for e in entity:
jsonfile.write(jsbeautifier.beautify(json.dumps(e.get_item())))
def daterange(start_date, end_date):
for n in range(int((end_date + datetime.timedelta(1) – start_date).days)):
yield start_date + datetime.timedelta(n)
def main():
numPersonas = 2
numCiudades = 2
minDate = datetime.date(2020,12,1)
maxDate = datetime.date(2020,12,31)
People = [Person(i) for i in range(1,numPersonas+1)]
Geo = [City(i) for i in range(1,numCiudades+1)]
Operations = []
for p in People:
for d in daterange(minDate, maxDate):
if(random.random() > 0.6):
Operations.append(Operation(p,d,Geo))
saveJson(People,‘People.json’)
saveJson(Geo,‘Geo.json’)
saveJson(Operations,‘Operations.json’)
main()
 
 

Incluímos una función daterange que simplifica la lógica de la lista de fechas a partir de una fecha de inicio y otra de final. Aquí, como se aprecia, definimos una fecha de inicio y de fin y, para cada persona de la lista recorremos las fechas generando un número aleatorio que comparamos con un valor arbitrario que determina si ese cliente realizó alguna operación en dicha fecha. Si es el caso, añadimos un elemento más a la lista de operaciones. Finalmente guardamos el json, que tiene la siguiente pinta:

generando datos prueba python ciudad

Una vez tenemos esta tabla de hechos, vamos a generar un volumen de datos más grande y a analizarlo en Power BI, para ver cómo se distribuyen los datos. Este análisis nos servirá para ir viendo la lógica que vamos a aplicar a la hora de generar las ventas.

A la hora de cargar en PowerBI hay que recordar que la tienda tiene un id dentro de la ciudad. Por lo tanto en la query M del json de Geo realizaremos la asignación de un nuevo id único a la tienda y el merge con las operaciones por los campos ciudad/tienda para poder montar el copo de nieve cuidad/tienda => operación.

Además, montamos una dimensión fecha con DAX, en función de la fecha de la primera y última operación.

Si configuramos el generador para que nos de 20 personas, 5 ciudades y todo el año 2020 de operaciones, con un umbral de 0.6 para generar operaciones, obtenemos la siguiente distribución:

Generando Datos de Prueba con Python para Power BI (Parte 2)

Esta estructura es indistinguible del ruido blanco, lo que indica que todos los datos son al azar y no hay ninguna ponderación ulterior. El dataset, en este estado, nos puede servir para probar modelos (añadiendo a lo mejor más dimensiones siguiendo el mismo patrón), pero no podremos demostrar nada. ¿Qué podríamos querer demostrar con un dataset de este tipo? Pues cosas como que podemos calificar a los clientes por los viajes que hacen, a las tiendas por el número de compras u otros cálculos derivados.

¡Vamos de Compras!

Habrá clientes que tengan más actividad o menos. En el análisis con PowerBI que hicimos antes, sin embargo, vemos que el número de operaciones de los clientes es muy parecido. La variabilidad del valor de las operaciones nace de la manera en la que generamos el valor de la operación a partir de números aleatorios, por lo que con seguridad, una muestra más grande de operaciones generaría un aplanamiento de esa curva, igualando los valores entre todos los clientes.

Así que lo que queremos es que cada cliente tenga un valor que determine si compra mucho o poco. La ejecución de esta idea es muy sencilla, dotemos al cliente de una propiedad Actividad que sea un número aleatorio y que se utilice como margen de la probabilidad de que una fecha determinada tenga compras. Así, en Person.py haremos.

class Person:
def __init__(self,i):
self.actividad = random.random()
 

Y en Generator.py añadiremos este valor a la expresión que determina si hay operación:

def main():
[]
for p in People:
for d in daterange(minDate, maxDate):
if(random.random() > (1 – p.actividad)):
Operations.append(Operation(p,d,Geo))
 

Ojo al dato: como hemos determinado un parámetro Actividad esto implica que queremos más operaciones cuanta más actividad. Por lógica semántica, el número al azar debe compararse con (1 – Actividad) para que más actividad arroje más probabilidad.

Si en este estado regeneramos los datos, vemos lo siguiente en Power BI:

generando datos prueba python metricas ciudad

Donde la variabilidad del número de operaciones depende del cliente: los hay con más y con menos operaciones.

¡Vamos de Compras!

En este punto tenemos clientes con más o menos actividad, pero todos realizan compras en lugares aleatorios.

generando datos prueba python metricas compras mapa

Nuestro cliente top 1, Nieves Bastida (en esta iteración, la próxima vez que se generen datos será otra persona) procede de una ciudad aleatoria, Guadalajara, y ha comprado una cantidad más o menos igual de veces en diferentes ciudades que nada tienen que ver con nuestras tiendas.

Así, necesitaríamos que, en primer lugar, los clientes fuesen oriundos de las ciudades en las que tenemos tienda (quizás con alguna probabilidad de que fuese de otra ciudad) y luego, que compren preferentemente en las tiendas de su ciudad, y no por ahí repartidos por España sin ton ni son. Para que las personas utilicen las ciudades, evidentemente necesitamos generar las ciudades antes que las personas.

Y necesitamos asignar una ciudad de la lista al generador de personas:

Geo = [City(i) for i in range(1,numCiudades+1)]
People = [Person(i,random.choice(Geo)) for i in range(1,numPersonas+1)]
 

El constructor de la persona, por lo tanto, debe recibir un objeto City y asignarle el ID directamente. Para poder manejarlo mejor, añadiremos a Person los campos id_ciudad y ciudad lo que nos permitirá comprobar qué ciudad tiene asignada la persona sin modificar el modelo de powerbi. Por último, sobre persona, vamos a generar un atributo mobilidad que determinará la probabilidad de comprar fuera de su ciudad de origen. Como lo que queremos es que haya una gran mayoría de casos en los que se compre en la ciudad de origen, vamos a hundir el valor de movilidad multiplicando tres números aleatorios menores que cero:

class Person:
def __init__(self,i,city):
[]
self.city = city
self.mobility = (random.random()*random.random()*random.random())
def get_item(self):
p = {
[]
‘ciudad’: self.city.name,
‘id_ciudad’: self.city.id
}
return (p)
 

Finalmente, el generador de operaciones debe, primero, mirar a qué ciudad corresponde el cliente y, después, determinar si el cliente se ha movido o no.

class Operation:
def __init__(self,person,date,geo):
[]
self.city = None
if (random.random() > person.mobility):
self.city = person.city
else:
self.city = random.choice(geo)
self.store = random.choice(self.city.stores)
 

Como guardamos una entidad city entera en la persona, podemos seguir eligiendo la tienda al azar de la lista de tiendas de la ciudad,

Con estos cambios, generamos y refrescamos el Power BI:

Generando Datos de Prueba con Python para Power BI (Parte 2)

Y ahora tenemos un set de clientes que compran mayoritariamente en su ciudad de origen.

¡Vacaciones y Rebajas!

Por último, vamos a modelar la estacionalidad de las compras. Esta afecta tanto al volumen de compras como al valor de las compras.

La idea implica mirar en la fecha si cumple algunas condiciones y modificar con ellas los parámetros que generan las operaciones. En primer lugar, para implementar la estacionalidad de las ventas (las vacaciones) podemos simplemente multiplicar la probabilidad de tener una operación por un valor que dependa del mes en el que estamos. Así, si estamos en junio, julio o agosto, las ventas serán un poco menos probables. La manera más rápida de implementar esta situación es escribiendo sin más la regla en una función que reciba la fecha y devuelva un factor para multiplicar la probabilidad:

def vacations(date):
if date.month in [6,7,8]:
return 0.8
else:
return 1
 

Así, en el generador de operaciones:

def main():
[]
for p in People:
print(‘%s de %s’ % (p.id,len(People)))
for d in daterange(minDate, maxDate):
if(vacations(d) * random.random() > (1 – p.actividad)):
Operations.append(Operation(p,d,Geo))

De esta manera queda modificada la probabilidad de que aparezca una venta en los meses deseados, lo que puede apreciarse en el Power BI al refrescar y mostrar el número de operaciones por mes:

generando datos prueba python metricas compras grafico PBI

Las rebajas, sin embargo, afectan al valor de las ventas, por lo que vamos a realizar la misma estrategia pero esta vez dentro de la operación.

Definimos una función para las rebajas que, de manera análoga a las vacaciones, arrojará un factor para multiplicar al valor:

def rebajas(date):
if date.month == 1:
return 0.6
else:
return 1
 

Y la invocamos en el constructor de las operaciones:

class Operation:
def __init__(self,person,date,geo):
[..]
self.valor = round(rebajas(date)*random.random()*random.random()*10000)

Además, podemos generar una nueva función de probabilidad de venta para subir la probabilidad en enero:

def rebajasProb(date):
if date.month in [1]:
return 1
else:
return 0.8
if(rebajasProb(d)*vacations(d) * random.random() > (1 – p.actividad)):
 

Y regeneramos con la estacionalidad de probabilidades y valores:

Generando Datos de Prueba con Python para Power BI (Parte 2)

Vemos cómo en Enero ahora hay más operaciones de menos valor medio, y se aprecia más claramente que antes el parón del verano.

¿Necesitas sacar más partido a tus datos para mejorar tu proyecto?

Acelera tus procesos de Business Analytics. Toma mejores decisiones, optimiza tu Datawarehouse y el proceso de generación de informes en tus proyectos de analítica con nuestra consultoría en Business Intelligence.

Voy a echar un vistazo
0 Shares:
Deja una respuesta

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

You May Also Like
Leer más

SQL Server downgrades: Enterprise Edition a Standard Edition

Un problema habitual al que nos tenemos que enfrentar es el realizar downgrades de SQL Server Enterprise Edition a Standard Edition. Las razones pueden ser desde un error cuando se realizó el despliegue inicial hasta un cambio para obtener una reducción de costes en licenciamiento. La forma soportada para realizar este cambio pasa por una desinstalación completa de la instancia y una reinstalación.