I have users who would like to be able to modify what columns a table aggregates by. My issue is that I seem unable to do this in Power BI. I basically want to be able to do the following in SQL:
SELECT
<OrgLevel1>,
<OrgLevel2>,
SUM([Revenue])
FROM [Data]
GROUP BY
<OrgLevel1>,
<OrgLevel2>
;
where the user can change <OrgLevel1> and/or <OrgLevel2> to be any of { "(All)", [Department], [Product] }.
The issue may be related to this post: https://community.powerbi.com/t5/Desktop/Calculated-Column-Table-Change-Dynamically-According-to-Slicer/m-p/655991#M314800
Here's a link to a workbook that illustrates this issue, TestParameterizeGroupby.pbix (hosted by Google Drive). I've also included field definitions below with screenshots. Thanks for any help.
TestParameterizeGroupby.pbix
Link: TestParameterizeGroupby.pbix (hosted by Google Drive)
Problem
[Org Level 1] and [Org Level 2] fields are not recalculating from the users' selection. Only the default values are shown.
Expected result in table
"Org Level 1", "Org Level 2", "Revenue"
"(All)", "(All)", 28
Note
The purpose is to have parameterizable organization level fields so that the report user can aggregate by all, department, product, or both in either order.
Table and column definitions
'Data' = DATATABLE(
"Department",
STRING,
"Product",
STRING,
"Revenue",
DOUBLE,
{
{"DeptA", "ProdX", 5.0},
{"DeptA", "ProdY", 6.0},
{"DeptB", "ProdX", 10.0},
{"DeptB", "ProdY", 7.0}
}
)
'Data'[Org Level 1] = SWITCH(
'Org Level 1 Parameter'[Org Level 1 Parameter Value],
0,
"(All)",
1,
[Department],
2,
[Product]
)
// Problem: [Org Level 1] and [Org Level 2] fields are not recalculating from the users' selection. Only the default values are shown.
'Org Level 1' = DATATABLE(
"Org Level 1",
STRING,
"Org Level 1 Parameter",
INTEGER,
{
{"(0) (All)", 0},
{"(1) Department", 1},
{"(2) Product", 2}
}
)
'Org Level 1 Parameter'[Org Level 1 Parameter] = GENERATESERIES(0, 2, 1)
'Org Level 1 Parameter'[Org Level 1 Parameter Value] = SELECTEDVALUE('Org Level 1 Parameter'[Org Level 1 Parameter], 1)
Table 'Org Level 1' has a 1-1 relationship with 'Org Level 1 Parameter' on column [Org Level 1 Parameter].
The user selects the value for 'Data'[Org Level 1] by selecting the value for 'Org Level 1'[Org Level 1].
Tables and columns for [Org Level 2] are defined in the same way as [Org Level 1].
Screenshots
Report view:
Data view:
Model view:
Cross-reference to post in Power BI forum:
Power BI Forum: How to parameterize a column for aggregation
One solution to this is to add two list values parameters and use their values in Power Query M code to modify the database query. Lets assume that you have a table Data with columns Department, Product and Revenue. For simplicity I will add one more column, named Dummy Column, with all rows having the same value (e.g. null). I will explain why later in this post. So the table looks like this:
Then in your report specify a query when adding this table to your model (lets assume we will import it, but in general you can do this in DirectQuery too):
Now if you look the M code you will see the above query there:
Source = Sql.Database(".", "StackOverflow", [Query=" select ....
Now define couple of parameters, that the end-user can use to select how the data should be aggregated. Lets name them Level 1 and Level 2:
The value of a parameter can be used in M by parameter name, and & is used to concatenate strings. So if there is a parameter Name with value Samuel, the expression "Hello, " & Name & "!" will be evaluated as Hello, Samuel!. The idea is to check the value of our parameters and modify the database query accordingly.
In the select part, we will replace the name of the field selected, or we will put '' (empty string) in case of <All> (I surrounded parameter values with brackets to be more easily to distinguish parameter values from database field names). So the expression should look like:
"select " & (if #"Level 1" = "<Department>" then "Department" else ..." (and so on)
Because there is a space in our parameter's name, we need to surround it with #" and ", so Level1 can be referenced simply as Level1 in the code, but Level 1 becomes #"Level 1".
The group by part is a bit trickier. We should add a comma between field names, add or not field name, or even omit the group by at all (in case both parameters are set to <All>). To simplify this, I added one dummy column, with all rows having the same value (e.g. null) and always group by this column. This way building the group by clause is way more simpler - in case the parameter value is not <All>, we should add , fieldname. So the code could look like this:
"group by DummyColumn" & (if #"Level 1" = "<Department>" then ", Department" else ..." (and so on)
So the final M code is this:
let
Source = Sql.Database(".", "StackOverflow", [Query="select#(lf) " & (if #"Level 1" = "<Department>" then "Department" else if #"Level 1" = "<Product>" then "Product" else "''") & " as [Org Level 1]#(lf) , " & (if #"Level 2" = "<Department>" then "Department" else if #"Level 2" = "<Product>" then "Product" else "''") & " as [Org Level 2]#(lf) , SUM(Revenue) as Revenue#(lf)from Data#(lf)group by DummyColumn" & (if #"Level 1" = "<Department>" then ", Department" else if #"Level 1" = "<Product>" then ", Product" else "") & (if #"Level 2" = "<Department>" then ", Department" else if #"Level 2" = "<Product>" then ", Product" else "")])
in
Source
Now the end-user can change parameter values, by clicking Edit Queries -> Edit Parameters:
And select how to group the data:
By default, Power BI Desktop will warn you first time, when particular query is executed:
If you want to turn this off, go to File -> Options and settings -> Options -> (GLOBAL) Security and make sure Require user approval for new native database queries is not selected:
When the end-user changes parameter values, the data will change too, e.g.:
Or:
And so on...
This trick works well in Power BI Desktop when every user has its own copy of the .pbix file. However, if you publish it, first changing parameter values is not very convenient (you must go to datasat's settings) and more important, changing parameter values affects all users, which are looking at this report. You can also use it to modify Table.Group statements generated by Power Query Editor, in case you want to aggregate the data in Power BI, but changing the database query is easier and more flexible.
If you want to enable this scenario for concurrent multi-users scenarios for published reports, you can use slicers and What-if parameters. Unfortunately, What-if parameters can be numeric (you can't define the list of values there), so you can use measures to "decode" the int value of the parameter and write some DAX code to perform different aggregations accordingly. It is more work, but if it is needed, it can be made too.
I'm writing a Drools rule set processing events of type A and B; and in many of the rules I need to compare A's attribute timestamp to maximum of a subset of B's attribute windowStart to qualify that A event. If that subset of B is an empty set, I assume there is no windowStart, and so the value 0 is desired. Any A's with the a timestamp greater than windowStart will qualify.
Consider the following pseudo code for elaboration:
long findMaxWindowStartOrZero(int bID)
{
Set bSubset = getAllBWithID(bID);
if(bSubset is empty) return 0;
return max(bSubset, B::windowStart);
}
without such query, LHS of every rule concerning this comparison needs to be duplicated, once to account for absence of any B and once to find the actual maximum when the subset is non-empty.
Having such query as the pseudo code above makes this task substantially easier, and removes the need to branch the LHS. Is it possible to do that? Is there any drawback or benefit for doing that instead of branching LHS of mentioned rules?
rule qualify_A
when
accumulate( B( id == "bID", $ws: windowStart ); $mws: max( $ws ) )
$a: A( timestamp > $mws )
then
...process $a...
end
I think that this rule does not fire if there are no matching Bs. To work around this, insert a B with windowStart set to 0. This dummy could also be used to define the value for matching B's id:
rule qualify_A
when
B( $id: id, windowStart == 0 ) // the dummy, defines id
accumulate( B( id == $id, $ws: windowStart ); $mws: max( $ws ) )
$a: A( timestamp > $mws )
then
...process $a...
end
I have a Decision table in Drools and I'm trying to retrieve the return values of the Conditions (columns) that evaluates to see if a particular Rule (rows) is executed. May I know how if it's possible? Please find below for a simple example of my problem.
Condition 1 | Condition 2 | Condition 3 | Condition 4
Age < 60 Employed=Yes Owns a house=Yes Single=Yes
Rule 1: YES YES
Rule 2: YES NO YES
Rule 3: NO YES
Let's say if Rule 2 should be ideally executed and yet Rule 1 is executed, I would like to know the reason why Rule 1 was executed by obtaining the return values of the Condition 1 and 3 (whether it is true or false). Is there a way to do so?
The DRL rule for your Rule 1 would be something like
rule "Rule 1"
when
Person( $age: age < 60, $owner: owner == true )
then
...( $age, $owner )...
end
and on the right hand side you have binding variables $age and $owner set to the actual values as contained in the Person fact. You can use Java code in an ACTION column in a decision table to do whatever you want with those values.
Edit If you need the values for the negative case, you'll have to use another rule
rule "Rule 2"
when
Person( $age: age, $owner: owner,
$age >= 60 || $owner == false )
then
another_action...( $age, $owner )...
end
Most likely, you'll need to do some other action anyway.
Of course, individual rules for all four cases are also possible. Note that decision tables let you write rules for all four cases TT, TF, FT, FF. You can combine the action cells so you can define the action for TF FT FF in a single cell.
I am trying to sort a list of objects using a set of rules defined in drools rules engine.
The sample object structure is as follows
public class A {
String name;
Date createdDate;
}
I want to
Define a set of rules for sorting a list of objects .
Ex : rule 1 : "Sort objects using name ascending"
rule 2 : "Sort objects using createdDate descending"
Define the order which the rules needs to be executed .
Ex : set order 1 to rule 1
set order 2 to rule 2
So the objects will be sorted by name ascending and createdDate descending.
Can I achieve this using the drools engine ?
I thought of using the compareTo() for the sorting but since the sorting criteria can be changed
at runtime the logic is becoming complex and hard to maintain.
Thanks,
Kolitha.
Drools does not sort objects the way quicksort or some similar sorting algorithm rearranges objects within an array or some other aggregate. What you can do is to have a rule fire, repeatedly, once for each fact from a set of facts (such as your class A objects) and with constraints guaranteeing this happening in a certain order. Also, you need to keep track of
facts that have already been processed.
Therefore, the question to be answered first is: why do you need the objects in a certain order?
If the facts need to be processed in this order, you don't have to sort them in the ususual sense of the word, and the aforementioned rule would be sufficient:
declare ListOfA
listOfA: List
end
rule noListOfA
when
not ListOfA()
then
ListOfA loa = new ListOfA();
loa.setListOfA( new ArrayList() );
insert( loa );
end
rule sortA
when
$a: A( $name: name, $createdDate: createdDate )
$loa: ListOfA( $listOfA: listOfA not contains $a )
not A( this != $a, this not memberOf $listOfA,
name < $name ||
name == $name && createdDate > $createdDate )
then
System.out.println( $a );
modify( $loa ){ getListOfA().add( $a ) }
end
This is the way to sort in drools.
rule "Rule 03"
when
$number : Number( )
not Number( intValue < $number.intValue )
then
System.out.println("Number found with value: " + $number.intValue() );
retract( $number );
end
I have a form that prompt for customer name and pass that value to a query,
FORM compname
customer.cusname
WITH FRAME f1.
UPDATE compname WITH FRAME f1.
This form wil pass the compname value to the following query,
FOR EACH customer WHERE customer.name = compname NO-LOCK :
if available(company) then
do:
CREATE temptt.
assign temptt.num = customer.kco
temptt.no = string(customer.kco)
temptt.name = customer.name
temptt.status = false.
END.
else
message "not matched " view-as alert-box.
end.
What i want to do is, if search does not receive any rows, it should again prompt for customer name. what should i do for this ??
how do i call that form again in the "else block" and also, currently I am giving the complete name in the field, but i want to give part of the name, for eg., customer name is "John Smith Doe" and if i input "Smith" it should retrieve the related rows. How should i alter the "Where" clause for this ?? Please help me.
Repeating the search
This can be done in several ways. Here's one example:
DEFINE TEMP-TABLE customer NO-UNDO
FIELD cusname AS CHARACTER
FIELD num AS INTEGER.
DEFINE VARIABLE compnum AS INTEGER NO-UNDO.
DEFINE VARIABLE compname AS CHARACTER NO-UNDO.
DEFINE QUERY qSearch FOR customer.
FORM compname compnum WITH FRAME f1.
/* Create some bogus data */
CREATE customer.
ASSIGN customer.cusname = "john doe"
customer.num = 1.
CREATE customer.
ASSIGN customer.cusname = "jane doe"
customer.num = 2.
CREATE customer.
ASSIGN customer.cusname = "name name"
customer.num = 3.
loop:
REPEAT:
CLEAR FRAME f2 ALL.
UPDATE compname compnum WITH FRAME f1.
/* Quit if neither name or number is entered */
IF compname = "" AND compnum = 0 THEN
LEAVE loop.
/* If num is entered - search by it, otherwise by name */
IF compnum <> 0 THEN DO:
OPEN QUERY qSearch FOR EACH customer NO-LOCK WHERE customer.num = compnum.
END.
ELSE DO:
OPEN QUERY qSearch FOR EACH customer NO-LOCK WHERE customer.cusname MATCHES "*" + compname + "*".
END.
GET NEXT qSearch.
DO WHILE AVAILABLE customer:
IF AVAILABLE customer THEN DO:
DISPLAY customer WITH FRAME f2 10 DOWN.
DOWN WITH FRAME f2.
END.
GET NEXT qSearch.
END.
/* If we have results - leave the loop otherwise try again */
IF QUERY qSearch:NUM-RESULTS = 0 THEN
LEAVE loop.
END.
MESSAGE "Quitting" VIEW-AS ALERT-BOX.
Searching for part of the name
There are a couple of operators for matching strings:
BEGINS
Tests a character expression to see if that expression begins with a second character expression.
Syntax:
expression1 BEGINS expression2
Example:
FOR EACH customer WHERE NO-LOCK customer.cusname BEGINS "john":
MATCHES
Compares a character expression to a pattern and evaluates to a TRUE value if the expression satisfies the pattern criteria.
The pattern can contain wildcard characters: a period (.) in a particular position indicates that any single character is acceptable in that position; an asterisk (*) indicates that any group of characters is acceptable, including a null group of characters.
Syntax:
expression1 MATCHES expression2
Example:
FOR EACH customer NO-LOCK WHERE customer.cusname MATCHES "*doe*":
MATCHES sounds like what you're after but be adviced: MATCHES will not utilize indices in the database so whole tables will be scanned. This can/will effect performance and possibly make your queries take long time.
The WHERE clause above replaced with MATCHES would look something like this:
FOR EACH customer NO-LOCK WHERE customer.cusname MATCHES "*" + compname + "*":
CONTAINS
There's also a third operator called CONTAINS that uses something called a WORD index. That will require you or your DBA to create these kind of indices in the database first. Read more about word indices and CONTAINS in the online help or in the PDF found here: Progress ABL Reference (page 1004).
CONTAINS is probably a better idea than MATCHES but will require you to make changes to your database as well.