*(For more resources related to this topic, see here.)*

This article examines the foundations of tabular modeling (at least from the modelers point of view). In order to provide a familiar model which can be used later, this article will progressively build the model. Each recipe is intended to demonstrate a particular technique; however, they need to be followed in order so that the final model is completed.

# Grouping by binning and sorting with ranks

Often, we want to provide descriptive information in our data, based on values that are derived from downstream activities. For example, this could arise if we wish to include a field in the *Customer* table that shows the value of the previous sales for that customer or a *bin grouping* that defines the customer into banded sales. This value can then be used to rank each customer according to their relative importance in relation to other customers.

This type of value adding activity is usually troublesome and time intensive in a traditional data mart design as the customer data can be fully updated only once the sales data has been loaded. While this process may seem relatively straightforward, it is a recursive problem as the customer data must be loaded before the sales data (since customers must exist before the sales can be assigned to them), but the full view of the customer is reliant on loading the sales data. In a standard dimensional (star schema) modeling approach, including this type of information for dimensions requires a three-step process:

- The dimension (reseller customer data) is updated for known fields and attributes. This load excludes information that is derived (such as sales).
- Then, the sales data (referred to as fact data) is loaded in data warehouse. This ensures that the data mart is in a
*current state*and all the sales transaction data is up-to-date. Information relating to any new and changed stores can be loaded correctly. - The dimension data which relies on other fact data is updated based on the current state of the data mart.

Since the tabular model is less confined by the traditional star schema requirements of the fact and dimension tables (in fact, the tabular model does not explicitly identify facts or dimensions), the inclusion and processing of these descriptive attributes can be built directly into the model.

*The calculation of a simple measure such as a historic sales value may be included in OLAP modeling through calculated columns in the data source view. However, this is restrictive and limited to simple calculations (such as total sales or n period sales). Other manipulations (such as ranking and binning) are a lot more flexible in tabular modeling (as we will see).*

This recipe examines how to manipulate a dimensional table in order to provide a richer end user experience. Specifically, we will do the following:

- Introduce a calculated field to calculate the historical sales for the customer
- Determine the rank of the customer based on that field
- Create a discretization bin for the customer based on their sales
- Create an ordered hierarchy based on their discretization bins

## Getting ready

Continuing with the scenario that was discussed in the *Introduction* section of the article, the purpose of this article is to identify each reseller’s (customer’s) historic sales and then rank them accordingly. We then discretize the *Resellers* table (customer) based on this. This problem is further complicated by the consideration that a sale occurs in the country of origin (the sales data in the *Reseller Sales* table will appear in any currency). In order to provide a concise recipe, we break the entire process into two distinct steps:

- Conversion of sales (which manages the ranking of
*Resellers*based on a unified sales value) - Classification of sales (which manages the manipulation of sales values based on discretized bins to format those bins)

## How to do it…

Firstly, we need to provide a common measure to compare the sales value of *Resellers*. Convert sales to a uniform currency using the following steps:

- Open a new workbook and launch the
**PowerPivot Window**. - Import the text files
*Reseller Sales.txt*,*Currency Conversion.txt*, and*Resellers.txt*.*The source data folder for this article includes the base**schema.ini*file that correctly transforms all data upon import. When importing the data, you should be prompted that the*schema.ini*file exists and will override the import settings. If this does not occur, ensure that the*schema.ini*file exists in the same directory as your source data. The prompt should look like the following screenshot:Although it is not mandatory, it is recommended that connection managers are labeled according to a standard. In this model, I have used the convention

*type_table_name*where*type*refers to the connection type (*.txt*) and*table_name*refers to the name of the table. Connections can be edited using the**Existing Connections**button in the**Design**tab. - Create a relationship between the
*Customer ID*field in the*Resellers*table and the*Customer ID*field in the*Reseller Sales*table. - Add a new field (also called a calculated column) in the
*Resellers Sales*table to show the gross value of the sale amount in USD. Add*usd_gross_sales*and hide it from client tools using the following code:= [Quantity Ordered] *[Price Per Unit] *LOOKUPVALUE ( 'Currency Conversion'[AVG Rate] ,'Currency Conversion'[Date] ,[Order dt] ,'Currency Conversion'[Currency ID] ,[Currency ID] )

- Add a new measure in the
*Resellers Sales*table to show sales (in USD). Add*USD Gross Sales*as:USD Gross Sales := SUM ( [usd_gross_sales] )

- Add a new calculated column to the
*Resellers*table to show the*USD Sales Total*value. The formula for the field should be:= 'Reseller Sales' [USD Gross Sales]

- Add a
*Sales Rank*field in the*Resellers*table to show the order for each resellers*USD Sales Total*. The formula for*Sales Rank*is:=RANKX(Resellers, [USD Sales Total])

- Hide the
*USD Sales Total*and*Sales Rank*fields from**client tools**.Now that all the entries in the

*Resellers*table show their sales value in a uniform currency, we can determine a grouping for the*Reseller*table. In this case, we are going to group them into 100,000 dollar bands. - Add a new field to show each value in the
*USD Sales Total*column of*Resellers*rounded down to the nearest 100,000 dollars. Hide it from client tools. Now, add*Round Down Amount*as:=ROUNDDOWN([USD Sales Total],-5)

- Add a new field to show the rank of
*Round Down Amount*in descending order and hide it from client tools. Add*Round Down Order*as:=RANKX(Resellers,[Round Down Amount],,FALSE(), DENSE)

- Add a new field in the
*Resellers*table to show the 100,000 dollars group that the reseller belongs to. Since we know what the lower bound of the sales bin is, we can also infer that the upper bin is the rounded up 100,000 dollars sales group. Add*Sales Group*as follows:=IF([Round Down Amount]=0 || ISBLANK([Round Down Amount]) , "Sales Under 100K" , FORMAT([Round Down Amount], "$#,K") & " - " & FORMAT(ROUNDUP([USD Sales Total],-5),"$#,K") )

- Set the
*Sort By Column*of the*Sales Group*field to the*Round Down Order*column. Note that the*Round Down Order*column should display in a descending order (that is, entries in the*Resellers*table with high sales values should appear first). - Create a
**hierarchy**on the*Resellers*table which shows the*Sales Group*field and the*Customer Name*column as**levels**. Title the hierarchy as*Customer By Sales Group*. - Add a new measure titled
*Number of Resellers*to the*Resellers*table:Number of Resellers:=COUNTROWS(Resellers)

- Create a pivot table that shows the
*Customer By Sales Group*hierarchy on the rows and*Number of Resellers*as values. If you created the*usd_gross_sales*field in the*Reseller Sales*table, it can also be added as an implicit measure to verify values. Expand the first bin of*Sales Group*. The pivot should look like the following screenshot:

## How it works…

This recipe has included various steps, which add descriptive information to the *Resellers* table. This includes obtaining data from a separate table (the USD sales) and then manipulating that field within the *Resellers* table. In order to provide a clearer definition of how this process works, we will break the explanation into several subsections. This includes the sales data retrieval, the use of rank functions, and finally, the discretization of sales.

The next section deals with the process of converting sales data to a single currency.

The starting point for this recipe is to determine a common currency sales value for each reseller (or customer). While the inclusion of the calculated column *USD Sales Total* in the *Resellers* table should be relatively straightforward, it is complicated by the consideration that the sales data is stored in multiple currencies. Therefore, the first step needs to include a currency conversion to determine the USD sales value for each line. This is simply the local value multiplied by the daily exchange rate. The *LOOKUPVALUE* function is used to return the row-by-row exchange rate.

Now that we have the *usd_gross_sales* value for each sales line, we define a measure that calculates its sum in whatever filter context it is applied in. Including it in the *Reseller Sales* table makes sense (since, it relates to sales data), but what is interesting is how the filter context is applied when it is used as a field in the *Resellers* table. Here, the **row filter context** that exists in the *Resellers* table (after all, each row refers to a reseller) applies a restriction to the sales data. This shows the sales value for each reseller.

For this recipe to work correctly, it is not necessary to include the calculated field *usd_gross_sales* in *Reseller Sales*. We simply need to define a calculation, which shows the gross sales value in USD and then use the row filter context in the *Resellers* table to restrict sales to the reseller in question (that is, the reseller in the row).

It is obvious that the exchange rate should be applied on a daily basis because the value can change every day. We could use an X function in the *USD Gross Sales* measure to achieve exactly the same outcome. Our formula will be:

SUMX ( 'Reseller Sales' , 'Reseller Sales'[Quantity Ordered] * 'Reseller Sales'[Price Per Unit] * LOOKUPVALUE ( 'Currency Conversion'[AVG Rate] , 'Currency Conversion'[Date] ,'Reseller Sales'[Order dt] ,'Currency Conversion'[Currency ID] ,'Reseller Sales'[Currency ID] ) )

Furthermore, if we wanted to, we could completely eliminate the *USD Gross Sales* measure from the model. To do this, we could wrap the entire formula (the previous definition on *USD Gross Sales*) into the *CALCULATE* statement in the *Resellers* table’s field definition of *USD Gross Sales*. This forces the calculation to occur at the current row context.

Why have we included the additional fields and measures in *Reseller Sales*? This is a modeling choice. It makes the model easier to understand because it is more modular. This would otherwise require two calculations (one into a default currency and the second into a selected currency) and the field *usd_gross_sales* is used in that calculation.

Now that sales are converted to a uniform currency, we can determine the importance by rank. *RANKX* is used to rank the rows in the *Resellers* table based on the *USD Gross Sales* field. The simplest implementation of *RANKX* is demonstrated within the *Sales Rank* field. Here, the function simply returns a rank based on the value according to the supplied measure (which is of course *USD Gross Sales*).

However, the *RANKX* function provides a lot of versatility and follows the syntax:

RANKX(<table> , <expression>[, <value>[, <order>[, <ties>]]] )

After the initial implementation of *RANKX* in its simplest form, the arguments of particular interest are the *<order>* and *<ties>* arguments. These can be used to specify the sort order (whether the rank is to be applied from highest to lowest or lowest to highest) and the function behavior when duplicate values are encountered. This may be best demonstrated with an example. To do this, we will examine the operation of rank in relation to *Round Down Amount*.

When a simple *RANKX* function is applied, the function sorts the columns in an ascending order and returns the position of a row based on the sorted order of the value and the number of prior rows within the table. This includes rows attributable to duplicate values. This is shown in the following screenshot where the *Simple* column is defined as *RANKX(Resellers,[Round Down Amount])*. Note, the data is sorted by *Round Down Amount* and the first four tied values have a *RANKX* value of *1*. This is the behavior we expect since all rows have the same value. For the next value (*700000*), *RANKX* returns *5* because this is the fifth element in the sequence.

When the *DENSE* argument is specified, the value returned after a tie is the next sequential number in the list. In the preceding screenshot, this is shown through the *DENSE* column. The formula for the field *DENSE* is:

RANKX(Resellers, [Round Down Amount],,,DENSE))

Finally, we can specify the sort order that is used by the function (the default is ascending) with the help of *<order>* argument of the function. If we wish to sort (and rank) from lowest to highest, we could use the formula as shown in the *INVERSE DENSE* column. The *INVERSE DENSE* column uses the following calculation:

RANKX(Resellers, [Round Down Amount],,TRUE,DENSE)

After having specified the *Sales Group* field **sort by column** as *Round Down Order*, we may ask why we did not also sort the *Customer Name* column by their respective values in the *Sales Rank* column? Trying to define a sort by column in this way would cause an error as it is not a one-to-one relationship between these two fields. That is, each customer does not have a unique value for the sales rank.

Let’s have a look at this in more detail. If we filter the *Resellers* table to show the blank *USD Sales Total* rows (click on the drop-down arrow in the *USD Sales Total* column and check the **BLANKS** checkbox), we see that the values of the *Sales Rank* column for all the rows is the same. In the following screenshot, we can see the value *636* repeated for all the rows:

*Allowing the client tool visibility to the USD Sales Total and Sales Rank fields will not provide an intuitive browsing attribute for most client tools. For this reason, it is not recommended to expose these attributes to users. Hiding them will still allow the data to be queried directly*

# Discretizing sales

By discretizing the *Resellers* table, we firstly make a decision to group each reseller into bands of 100,000 intervals. Since, we have already calculated the *USD Gross Sales* value for each customer, our problem is reduced by determining which bin each customer belongs to.

This is very easily achieved as we can derive the lower and upper bound for the *Resellers* table. That is, the lower bound will be a rounded down amount of their sales and the upper bound will be the rounded up value (that is rounded nearest to the 100,000 interval). Finally, we must ensure that the ordering of the bins is correct so that the bins appear from the highest value resellers to the lowest.

For convenience, these steps are broken down through the creation of additional columns but they need not be—we could incorporate the steps into a single formula (mind you, it would be hard to read). Additionally, we have provided a unique name for the first bin by testing for 0 sales. This may not be required.

The rounding is done with the *ROUNDDOWN* and *ROUNDUP* functions. These functions simply return the number moved by the number of digits offset. The following is the syntax for *ROUNDDOWN*:

ROUNDDOWN(<number>, <num_digits>)

Since we are interested only in the *INTEGER* values (that is, values to the left of the decimal place), we must specify *<num_digits>* as *-5*.

The display value of the bin is controlled through the *FORMAT* function, which returns the text equivalent of a value according to the provided format string. The syntax for *FORMAT* is:

FORMAT(<value>, <format_string>)

## There’s more…

In presenting a *USD Gross Sales* value for the *Resellers* table, we may not be interested in all the historic data. A typical variation on this theme is to determine the current worth by showing the recent history (or summing recent sales). This requirement can be easily implemented into the preceding method by swapping *USD Gross Sales* with recent sales. To determine this amount, we need to filter the data used in the *SUM* function. For example, to determine the last 30 days’ sales for a reseller, we will use the following code:

SUMX( FILTER('Reseller Sales' , 'Reseller Sales'[Order dt]> (MAX('Reseller Sales'[Order dt])-30) ) , USD SALES EXPRESSION )