Cálculo de la frecuencia de compra de los clientes
En este escenario partimos de un listado de clientes:
Drupal\user\Entity\Role::postLoad(Object, Array) (Line: 423) Drupal\Core\Entity\EntityStorageBase->postLoad(Array) (Line: 353) Drupal\Core\Entity\EntityStorageBase->loadMultiple(Array) (Line: 16) Drupal\user\RoleStorage->isPermissionInRoles('access site in maintenance mode', Array) (Line: 112) Drupal\Core\Session\UserSession->hasPermission('access site in maintenance mode') (Line: 105) Drupal\Core\Session\AccountProxy->hasPermission('access site in maintenance mode') (Line: 83) Drupal\redirect\RedirectChecker->canRedirect(Object) (Line: 120) Drupal\redirect\EventSubscriber\RedirectRequestSubscriber->onKernelRequestCheckRedirect(Object, 'kernel.request', Object) call_user_func(Array, Object, 'kernel.request', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.request') (Line: 145) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\views\ManyToOneHelper->__construct(Object) (Line: 54) Drupal\views\Plugin\views\argument\ManyToOne->defineOptions() (Line: 141) Drupal\views\Plugin\views\PluginBase->init(Object, Object, Array) (Line: 104) Drupal\views\Plugin\views\HandlerBase->init(Object, Object, Array) (Line: 82) Drupal\views\Plugin\views\argument\ArgumentPluginBase->init(Object, Object, Array) (Line: 33) Drupal\views\Plugin\views\argument\ManyToOne->init(Object, Object, Array) (Line: 894) Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers('argument') (Line: 1045) Drupal\views\ViewExecutable->_initHandler('argument', Array) (Line: 903) Drupal\views\ViewExecutable->initHandlers() (Line: 2318) Drupal\views\Plugin\views\display\DisplayPluginBase->preExecute() (Line: 1697) Drupal\views\ViewExecutable->preExecute(Array) (Line: 1632) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\views\ManyToOneHelper->__construct(Object) (Line: 54) Drupal\views\Plugin\views\argument\ManyToOne->defineOptions() (Line: 228) Drupal\views\Plugin\views\PluginBase->unpackOptions(Array, Array) (Line: 144) Drupal\views\Plugin\views\PluginBase->init(Object, Object, Array) (Line: 104) Drupal\views\Plugin\views\HandlerBase->init(Object, Object, Array) (Line: 82) Drupal\views\Plugin\views\argument\ArgumentPluginBase->init(Object, Object, Array) (Line: 33) Drupal\views\Plugin\views\argument\ManyToOne->init(Object, Object, Array) (Line: 894) Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers('argument') (Line: 1045) Drupal\views\ViewExecutable->_initHandler('argument', Array) (Line: 903) Drupal\views\ViewExecutable->initHandlers() (Line: 2318) Drupal\views\Plugin\views\display\DisplayPluginBase->preExecute() (Line: 1697) Drupal\views\ViewExecutable->preExecute(Array) (Line: 1632) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\views\ManyToOneHelper->__construct(Object) (Line: 54) Drupal\views\Plugin\views\argument\ManyToOne->defineOptions() (Line: 228) Drupal\views\Plugin\views\PluginBase->unpackOptions(Array, Array) (Line: 110) Drupal\views\Plugin\views\HandlerBase->init(Object, Object, Array) (Line: 82) Drupal\views\Plugin\views\argument\ArgumentPluginBase->init(Object, Object, Array) (Line: 33) Drupal\views\Plugin\views\argument\ManyToOne->init(Object, Object, Array) (Line: 894) Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers('argument') (Line: 1045) Drupal\views\ViewExecutable->_initHandler('argument', Array) (Line: 903) Drupal\views\ViewExecutable->initHandlers() (Line: 2318) Drupal\views\Plugin\views\display\DisplayPluginBase->preExecute() (Line: 1697) Drupal\views\ViewExecutable->preExecute(Array) (Line: 1632) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\views\ManyToOneHelper->__construct(Object) (Line: 35) Drupal\views\Plugin\views\argument\ManyToOne->init(Object, Object, Array) (Line: 894) Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers('argument') (Line: 1045) Drupal\views\ViewExecutable->_initHandler('argument', Array) (Line: 903) Drupal\views\ViewExecutable->initHandlers() (Line: 2318) Drupal\views\Plugin\views\display\DisplayPluginBase->preExecute() (Line: 1697) Drupal\views\ViewExecutable->preExecute(Array) (Line: 1632) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('views') (Line: 1316) Drupal\views\Plugin\views\query\Sql->query() (Line: 1454) Drupal\views\Plugin\views\query\Sql->build(Object) (Line: 1326) Drupal\views\ViewExecutable->build() (Line: 392) Drupal\views\Plugin\views\display\PathPluginBase->execute() (Line: 196) Drupal\views\Plugin\views\display\Page->execute() (Line: 1635) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('views_substitutions', Array) (Line: 1418) Drupal\views\Plugin\views\query\Sql->query() (Line: 1454) Drupal\views\Plugin\views\query\Sql->build(Object) (Line: 1326) Drupal\views\ViewExecutable->build() (Line: 392) Drupal\views\Plugin\views\display\PathPluginBase->execute() (Line: 196) Drupal\views\Plugin\views\display\Page->execute() (Line: 1635) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('views') (Line: 1316) Drupal\views\Plugin\views\query\Sql->query(1) (Line: 1455) Drupal\views\Plugin\views\query\Sql->build(Object) (Line: 1326) Drupal\views\ViewExecutable->build() (Line: 392) Drupal\views\Plugin\views\display\PathPluginBase->execute() (Line: 196) Drupal\views\Plugin\views\display\Page->execute() (Line: 1635) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('views_substitutions', Array) (Line: 1418) Drupal\views\Plugin\views\query\Sql->query(1) (Line: 1455) Drupal\views\Plugin\views\query\Sql->build(Object) (Line: 1326) Drupal\views\ViewExecutable->build() (Line: 392) Drupal\views\Plugin\views\display\PathPluginBase->execute() (Line: 196) Drupal\views\Plugin\views\display\Page->execute() (Line: 1635) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\views\ManyToOneHelper->__construct(Object) (Line: 54) Drupal\views\Plugin\views\argument\ManyToOne->defineOptions() (Line: 141) Drupal\views\Plugin\views\PluginBase->init(Object, Object, Array) (Line: 104) Drupal\views\Plugin\views\HandlerBase->init(Object, Object, Array) (Line: 82) Drupal\views\Plugin\views\argument\ArgumentPluginBase->init(Object, Object, Array) (Line: 33) Drupal\views\Plugin\views\argument\ManyToOne->init(Object, Object, Array) (Line: 894) Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers('argument') (Line: 1045) Drupal\views\ViewExecutable->_initHandler('argument', Array) (Line: 903) Drupal\views\ViewExecutable->initHandlers() (Line: 1876) Drupal\views\ViewExecutable->buildTitle() (Line: 338) Drupal\views\Plugin\views\display\Feed->attachTo(Object, 'page_1', Array) (Line: 1733) Drupal\views\ViewExecutable->attachDisplays() (Line: 1333) Drupal\views\ViewExecutable->build() (Line: 392) Drupal\views\Plugin\views\display\PathPluginBase->execute() (Line: 196) Drupal\views\Plugin\views\display\Page->execute() (Line: 1635) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\views\ManyToOneHelper->__construct(Object) (Line: 54) Drupal\views\Plugin\views\argument\ManyToOne->defineOptions() (Line: 228) Drupal\views\Plugin\views\PluginBase->unpackOptions(Array, Array) (Line: 144) Drupal\views\Plugin\views\PluginBase->init(Object, Object, Array) (Line: 104) Drupal\views\Plugin\views\HandlerBase->init(Object, Object, Array) (Line: 82) Drupal\views\Plugin\views\argument\ArgumentPluginBase->init(Object, Object, Array) (Line: 33) Drupal\views\Plugin\views\argument\ManyToOne->init(Object, Object, Array) (Line: 894) Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers('argument') (Line: 1045) Drupal\views\ViewExecutable->_initHandler('argument', Array) (Line: 903) Drupal\views\ViewExecutable->initHandlers() (Line: 1876) Drupal\views\ViewExecutable->buildTitle() (Line: 338) Drupal\views\Plugin\views\display\Feed->attachTo(Object, 'page_1', Array) (Line: 1733) Drupal\views\ViewExecutable->attachDisplays() (Line: 1333) Drupal\views\ViewExecutable->build() (Line: 392) Drupal\views\Plugin\views\display\PathPluginBase->execute() (Line: 196) Drupal\views\Plugin\views\display\Page->execute() (Line: 1635) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\views\ManyToOneHelper->__construct(Object) (Line: 54) Drupal\views\Plugin\views\argument\ManyToOne->defineOptions() (Line: 228) Drupal\views\Plugin\views\PluginBase->unpackOptions(Array, Array) (Line: 110) Drupal\views\Plugin\views\HandlerBase->init(Object, Object, Array) (Line: 82) Drupal\views\Plugin\views\argument\ArgumentPluginBase->init(Object, Object, Array) (Line: 33) Drupal\views\Plugin\views\argument\ManyToOne->init(Object, Object, Array) (Line: 894) Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers('argument') (Line: 1045) Drupal\views\ViewExecutable->_initHandler('argument', Array) (Line: 903) Drupal\views\ViewExecutable->initHandlers() (Line: 1876) Drupal\views\ViewExecutable->buildTitle() (Line: 338) Drupal\views\Plugin\views\display\Feed->attachTo(Object, 'page_1', Array) (Line: 1733) Drupal\views\ViewExecutable->attachDisplays() (Line: 1333) Drupal\views\ViewExecutable->build() (Line: 392) Drupal\views\Plugin\views\display\PathPluginBase->execute() (Line: 196) Drupal\views\Plugin\views\display\Page->execute() (Line: 1635) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\views\ManyToOneHelper->__construct(Object) (Line: 35) Drupal\views\Plugin\views\argument\ManyToOne->init(Object, Object, Array) (Line: 894) Drupal\views\Plugin\views\display\DisplayPluginBase->getHandlers('argument') (Line: 1045) Drupal\views\ViewExecutable->_initHandler('argument', Array) (Line: 903) Drupal\views\ViewExecutable->initHandlers() (Line: 1876) Drupal\views\ViewExecutable->buildTitle() (Line: 338) Drupal\views\Plugin\views\display\Feed->attachTo(Object, 'page_1', Array) (Line: 1733) Drupal\views\ViewExecutable->attachDisplays() (Line: 1333) Drupal\views\ViewExecutable->build() (Line: 392) Drupal\views\Plugin\views\display\PathPluginBase->execute() (Line: 196) Drupal\views\Plugin\views\display\Page->execute() (Line: 1635) Drupal\views\ViewExecutable->executeDisplay('page_1', Array) (Line: 81) Drupal\views\Element\View::preRenderViewElement(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-350e337f-140f-4fa2-bda4-0f03477cb3c2') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-350e337f-140f-4fa2-bda4-0f03477cb3c2') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-350e337f-140f-4fa2-bda4-0f03477cb3c2') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-350e337f-140f-4fa2-bda4-0f03477cb3c2') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-03329b88-76e4-4c9b-8765-af78475b80e1') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-03329b88-76e4-4c9b-8765-af78475b80e1') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-03329b88-76e4-4c9b-8765-af78475b80e1') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-03329b88-76e4-4c9b-8765-af78475b80e1') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-e53eaa7f-c9ca-4db7-8f55-7fbf3b792f47') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-e53eaa7f-c9ca-4db7-8f55-7fbf3b792f47') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-e53eaa7f-c9ca-4db7-8f55-7fbf3b792f47') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-e53eaa7f-c9ca-4db7-8f55-7fbf3b792f47') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-5c3969df-7095-40d4-b46e-af97df4d2797') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-5c3969df-7095-40d4-b46e-af97df4d2797') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-5c3969df-7095-40d4-b46e-af97df4d2797') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-5c3969df-7095-40d4-b46e-af97df4d2797') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-0c5b04ec-58bd-46b2-a673-efde628eabbb') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-0c5b04ec-58bd-46b2-a673-efde628eabbb') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-0c5b04ec-58bd-46b2-a673-efde628eabbb') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-0c5b04ec-58bd-46b2-a673-efde628eabbb') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-b68efa25-a5d8-4ce5-8aa3-96568aef9119') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-b68efa25-a5d8-4ce5-8aa3-96568aef9119') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-b68efa25-a5d8-4ce5-8aa3-96568aef9119') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-b68efa25-a5d8-4ce5-8aa3-96568aef9119') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-a1dd98da-72fb-45f2-a860-f2a054bc67d1') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-a1dd98da-72fb-45f2-a860-f2a054bc67d1') (Line: 95) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addMetaData('entity_type', 'file') (Line: 115) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-a1dd98da-72fb-45f2-a860-f2a054bc67d1') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\Core\Database\Query\Select->addTag('entity_query') (Line: 147) Drupal\Core\Entity\Query\Sql\Query->prepare() (Line: 80) Drupal\Core\Entity\Query\Sql\Query->execute() (Line: 640) Drupal\Core\Entity\EntityStorageBase->loadByProperties(Array) (Line: 63) Drupal\Core\Entity\EntityRepository->loadEntityByUuid('file', 'insert-max_800_px-a1dd98da-72fb-45f2-a860-f2a054bc67d1') (Line: 124) Drupal\editor\Plugin\Filter\EditorFileReference->process('En este escenario partimos de un listado de clientes: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0024.jpg"></a> ...y de una tabla de compras de cada uno de ellos: <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0025.jpg"></a> El objetivo es obtener un listado de clientes en el que se muestre el número de días (por ejemplo, podría ser otro período) entre compras. Para hacer el escenario sencillo los clientes reciben como nombre "Cliente x", siendo x el número de compras realizadas (de esta forma, el <em>Cliente 0</em> no ha realizado ninguna, el <em>Cliente 1</em> ha realizado una, etc.), y la frecuencia de compra en días para cada cliente coincide también con x. Así, el cliente 2 realizó una primera compra el 3 de enero, y una segunda dos días más tarde, el 5 de enero. Una vez hemos asegurado que el modelo de datos incluye las relaciones adecuadas (incluyendo una relación entre la tabla de ventas y el necesario calendario), podemos crear una tabla calculada que, usando la función <a href="/dax/function/selectcolumns">SELECTCOLUMNS</a>, extraiga el listado de clientes y agregue la información necesaria. Partimos, por lo tanto, de la siguiente expresión DAX que define esta primera versión de nuestra tabla de resultados: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre] ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0026.jpg"></a> Aunque podríamos calcular directamente la frecuencia de compra para cada uno de ellos, hagámoslo paso por paso, incluyendo una columna adicional para cada uno de los datos que necesitamos. Si entendemos por frecuencia de compra el número de días transcurridos entre la primera compra y la última dividido entre el número de compras realizadas, en primer lugar sería preciso calcular las fechas de primera y de última compra por cliente, para lo que vamos a recurrir a las funciones <a href="/dax/function/firstdate">FIRSTDATE</a> y <a href="/dax/function/lastdate">LASTDATE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0027.jpg"></a> El cálculo del rango de días entre ambas fechas se reduce a una sencilla resta, que realizaremos usando la función <a href="/dax/function/datediff">DATEDIFF</a>, función que nos permite especificar el período a considerar -días, en nuestro ejemplo-: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0028.jpg"></a> Debemos entender que estamos realizando un enfoque un tanto ingenuo del problema al presuponer que el cliente existe como tal desde su primera compra y solo hasta la última. Más realista sería considerar como comienzo del período el día en el que el cliente "se dio de alta como tal", hubiese realizado o no una compra, y/o considerar como fin del período el día actual, por ejemplo. Considérese también que, al crearse la tabla calculada en un entorno de contexto de fila, para cada cliente se extrae la información de la tabla remota relacionada con él. Así, cuando se está calculando la primera fecha de compra para el cliente X, la expresión FIRSTDATE(Calendario[Fecha]) ...filtra el campo <em>Fecha</em> de la tabla <em>Calendario</em> de forma que solo incluya las fechas para las que existen compras asociadas al cliente que corresponda. El siguiente paso es contar el número de compras por cliente, para lo que podemos extraer la tabla <em>Ventas</em> (una vez filtrada por el contexto, para lo que usaremos la función <a href="/dax/function/relatedtable">RELATEDTABLE</a>) y contar el número de filas que tiene con <a href="/dax/function/countrows">COUNTROWS</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0029.jpg"></a> Por último queda lo más sencillo, que es dividir el rango de fechas entre el número de compras, para lo que recurriremos a la función <a href="/dax/function/divide">DIVIDE</a>: Frecuencia de compra = SELECTCOLUMNS( Clientes; "Nombre"; Clientes[Nombre]; "Primera compra"; FIRSTDATE(Calendario[Fecha]); "Última compra"; LASTDATE(Calendario[Fecha]); "Rango"; DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); "Nº de compras"; COUNTROWS(RELATEDTABLE(Ventas)); "Frecuencia de compra"; DIVIDE( DATEDIFF(FIRSTDATE(Calendario[Fecha]); LASTDATE(Calendario[Fecha]); DAY); COUNTROWS(RELATEDTABLE(Ventas)) ) ) <a class="colorbox insert-colorbox" data-colorbox-gallery="gallery-node" data-insert-class="" data-insert-type="image" href="/sites/default/files/2019-10/dax-0030.jpg"></a> Comprobamos que, en esta primera versión de la expresión, si un cliente no ha realizado ninguna compra se muestra un BLANK, y si ha realizado una única compra de devuelve una frecuencia de 1. Podríamos completar nuestro DIVIDE con alguna función lógica que realizase un cálculo u otro en función del número de compras y, de esta forma, gestionar estas excepciones. Puede descargarse el fichero Excel y el informe Power BI resultante <a href="https://www.interactivechaos.com/sites/default/files/data/calculo_de_la_frecuencia_de_compra_de_los_clientes.zip">aquí</a>. ', 'es') (Line: 118) Drupal\filter\Element\ProcessedText::preRenderText(Array) call_user_func_array(Array, Array) (Line: 101) Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 788) Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 374) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 88) __TwigTemplate_a7d6005c89ae729617b9a0c2bccb1776->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 46) __TwigTemplate_804f7948456cfe20e11a34c43439c7c2->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array, Array) (Line: 43) __TwigTemplate_bd990293b89f3b78c69fe0ee2f7828b5->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/field/field--text-with-summary.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('field', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_94047fbdba6937b76a4479dfa1763452->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/node.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('node', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 74) __TwigTemplate_43dffa6ad507293d1ceeb24e05ce942c->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/custom/yg_aesthetic/templates/views-view-unformatted.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view_unformatted', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array) (Line: 479) Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110) __TwigTemplate_349d2f5aada73507d566397721f27ea4->doDisplay(Array, Array) (Line: 405) Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378) Twig\Template->display(Array) (Line: 390) Twig\Template->render(Array) (Line: 55) twig_render_template('themes/contrib/classy/templates/views/views-view.html.twig', Array) (Line: 384) Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 433) Drupal\Core\Render\Renderer->doRender(Array) (Line: 446) Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 204) Drupal\Core\Render\Renderer->render(Array, ) (Line: 242) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 580) Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 235) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
include('/var/www/vhosts/interactivechaos.ovh/httpdocs/modules/contrib/google_analytics/src/Component/Render/GoogleAnalyticsJavaScriptSnippet.php') (Line: 578) Composer\Autoload\ClassLoader::Composer\Autoload\{closure}('/var/www/vhosts/interactivechaos.ovh/httpdocs/modules/contrib/google_analytics/src/Component/Render/GoogleAnalyticsJavaScriptSnippet.php') (Line: 432) Composer\Autoload\ClassLoader->loadClass('Drupal\google_analytics\Component\Render\GoogleAnalyticsJavaScriptSnippet') (Line: 372) google_analytics_page_attachments(Array) (Line: 313) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}(Object, 'google_analytics') (Line: 405) Drupal\Core\Extension\ModuleHandler->invokeAllWith('page_attachments', Object) (Line: 310) Drupal\Core\Render\MainContent\HtmlRenderer->invokePageAttachmentHooks(Array) (Line: 288) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
Drupal\user\Entity\Role::postLoad(Object, Array) (Line: 423) Drupal\Core\Entity\EntityStorageBase->postLoad(Array) (Line: 353) Drupal\Core\Entity\EntityStorageBase->loadMultiple() (Line: 126) eu_cookie_compliance_page_attachments(Array) (Line: 313) Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}(Object, 'eu_cookie_compliance') (Line: 405) Drupal\Core\Extension\ModuleHandler->invokeAllWith('page_attachments', Object) (Line: 310) Drupal\Core\Render\MainContent\HtmlRenderer->invokePageAttachmentHooks(Array) (Line: 288) Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132) Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90) Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) call_user_func(Array, Object, 'kernel.view', Object) (Line: 142) Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 174) Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81) Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58) Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191) Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128) Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82) Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 48) Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51) Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23) Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 713) Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
En este escenario partimos de un listado de clientes:
Un escenario frecuente es el que supone el cálculo de crecimiento de una métrica expresado como porcentaje (ventas, número de clientes, número de unidades vendidas, etc.) con respecto a un cierto período anterior, por ejemplo un año o un trimestre, por poner un par de ejemplos. El cálculo básico a realizar es el siguiente:
[% crecimiento] = (Valor actual - Valor hace un año) / Valor hace un año
Excepto donde se indique otra cosa, los contenidos de este sitio web se ofrecen bajo una licencia Reconocimiento-NoComercial-SinObraDerivada 4.0 Internacional