Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Don't miss out! 2025 Microsoft Fabric Community Conference, March 31 - April 2, Las Vegas, Nevada. Use code MSCUST for a $150 discount. Prices go up February 11th. Register now.

Reply
Insert_Key
Frequent Visitor

Need help restructuring a table (Pivot / Unpivot?)

Hi everyone 👋🙂

 

I was assigned a task right at the end of the day that feels so straightforward to me but I've just been chasing my tail on and it's doing my head in: I've been asked to transform a table so that a system user's details can be viewed on a single row instead of spanning multiple rows. I felt that the answer would lie with Pivot and/or Unpivot, but ran out of patience (and skill!) before I was unable to create the desired results... Pivoting/Unpivoting data confuses me every single time! I've added a small set of dummy data and the result that I am chasing below.

 

Sample:

OWNERGROUPIDNAMELOG
HRTEAMCREATEHR_123JOHN SMITH1/06/2024
HRTEAMEDITHR_123JOHN SMITH1/06/2024
HRTEAMDELETEHR_123JOHN SMITH1/06/2024
HRTEAMIMPORTHR_123JOHN SMITH1/06/2024
HRTEAMEXPORTHR_123JOHN SMITH1/06/2024
HRTEAMLEGALHR_XYZJOHN SMITH1/06/2024
HRTEAMDIRECTORHR_XYZJOHN SMITH1/06/2024

 

Result:

OWNERID 1ID 2NAMELOGGROUP 1GROUP 2GROUP 3GROUP 4GROUP 5GROUP 6GROUP 7
HRTEAMHR_123HR_XYZJOHN SMITH1/06/2024CREATEEDITDELETEIMPORTEXPORTLEGALDIRECTOR

 

My mind went to Pivot / Unpivot but if anyone has a better or preferred solution, please pitch in - I'm not constrained to using Pivot / Unpivot at all. Thanks in advance for your help! 😊

EDIT: Here is a link to a file containing the dummy data; hopefully you can still download and edit even though the document is set to read-only on OneDrive. Please let me know if there are any issues.

1 ACCEPTED SOLUTION

@Insert_Key my bad, I forgot that Table.FromRecords takes resulting columns from the very first record in the list. Then lets create tables from each group of data and combine them. Try this code. 

 

let
    Source = Excel.CurrentWorkbook(){[Name="Table5"]}[Content],
    // function uses table column name to create a record of distinct values 
    fx = (tbl, col) => 
        [values = List.Distinct(Table.Column(tbl, col)),
        count = List.Count(values),
        names = if count = 1 then {col} else List.Transform({1..count}, (x) => col & " " & Text.From(x)),
        out = Record.FromList(values, names)][out],
    // column names - you may filter unwanted columns
    columns = List.Buffer(Table.ColumnNames(Source)),
    // control "group by" list, now it produces OUTPUT B from your sample. To get OUTPUT A use {"NAME"}
    to_rows = Table.Group(
        Source, 
        {"NAME", "ID"}, 
        {"x", (x) => Table.FromRecords(
            {Record.Combine(List.Transform(columns, (w) => fx(x, w)))}
        )}),
    to_table = Table.Combine(to_rows[x])
in
    to_table

 

View solution in original post

9 REPLIES 9
dufoq3
Super User
Super User

Hi @Insert_Key, different approach:

 

Result

dufoq3_0-1719402924743.png

 

If you want to group by different columns, you have to edit GroupedRows step, but keep in mind that you have to change both lists (they should be the same):

 

dufoq3_1-1719403049517.png

 

Whole code:

let
    Source = Table.FromRows(Json.Document(Binary.Decompress(Binary.FromText("i45W8ggKcXX0VdJRcg5ydQxxBTI8guINjYyBDC9/Dz+FYF/PEA8gx1DfwEzfyMDIRClWB0mXq4tnCKl6XFx9XEm3ydM3wD+IZLtcI8jR5ePq7ugD0RQRGUWstzyDXJ1D/IOI0RcLAA==", BinaryEncoding.Base64), Compression.Deflate)), let _t = ((type nullable text) meta [Serialized.Text = true]) in type table [OWNER = _t, GROUP = _t, ID = _t, NAME = _t, LOG = _t]),
    ChangedType = Table.TransformColumnTypes(Source,{{"LOG", type date}}),
    GroupedRows = Table.Group(ChangedType, {"OWNER", "NAME", "LOG"}, {{"All", each _, type table}, {"fn", each 
        [ tbl = Table.FirstN(Table.SelectColumns(_, {"OWNER", "NAME", "LOG"}), 1),
          idList = List.Buffer(List.Distinct([ID])),
          groupList = List.Buffer(List.Distinct([GROUP])),
          idAcc = List.Accumulate({0..List.Count(idList)-1}, tbl, (s,c)=> Table.AddColumn(s, "ID " & Text.From(c+1), (x)=> idList{c}, type text)),
          groupAcc = List.Accumulate({0..List.Count(groupList)-1}, idAcc, (s,c)=> Table.AddColumn(s, "GROUP " & Text.From(c+1), (x)=> groupList{c}, type text))
        ][groupAcc], type table}}),
    CombinedFn = Table.Combine(GroupedRows[fn])
in
    CombinedFn

 


Note: Check this link to learn how to use my query.
Check this link if you don't know how to provide sample data.

Thank you very much for your solution, dufoq3, I really appreciate it 🤗 I'm far closer to being able to understand the code in the first reply though, so I will adopt that method in the hope that I can learn its structure and remember it for the next time that I need to undertake a similar task. It's always interesting to see different approaches; thanks again.

You're welcome 🙂


Note: Check this link to learn how to use my query.
Check this link if you don't know how to provide sample data.

AlienSx
Super User
Super User

OUTPUT B from your file. Read comments in code to get OUTPUT A or C

let
    Source = Excel.CurrentWorkbook(){[Name="Table5"]}[Content],
    // function uses table column name to create a record of distinct values 
    fx = (tbl, col) => 
        [values = List.Distinct(Table.Column(tbl, col)),
        count = List.Count(values),
        names = if count = 1 then {col} else List.Transform({1..count}, (x) => col & " " & Text.From(x)),
        out = Record.FromList(values, names)][out],
    // column names - you may filter unwanted columns
    columns = List.Buffer(Table.ColumnNames(Source)),
    // control "group by" list, now it produces OUTPUT B from your sample. To get OUTPUT A use {"NAME"}
    to_rows = Table.Group(Source, {"NAME", "ID"}, {"x", (x) => Record.Combine(List.Transform(columns, (w) => fx(x, w)))}),
    to_table = Table.FromRecords(to_rows[x], null, MissingField.UseNull)
in
    to_table

Thank you so much! That works amazingly - I've applied it to my own sample data and got it working without issue. Before I did that, I inserted your code into my working file/real data and it does everything apart from fully expand the List into columns 😐 I don't understand it. Works as per the sample file up to and including "to_rows" but the "to_table" step results in only the inital five columns presenting.

 

I can click to expand the list at the "to_rows" step which adds "= Table.ExpandRecordColumn(to_rows, "x", {"OWNER", "GROUP", "ID", "NAME", "LASTJOB", "GROUP 1", "GROUP 2", "GROUP 3", "GROUP 4", "GROUP 5"}, {"OWNER", "GROUP", "ID", "NAME.1", "LASTJOB", "GROUP 1", "GROUP 2", "GROUP 3", "GROUP 4", "GROUP 5"})" as a step and works fine, but I am curious to understand your code and resolve the issue as it is much more elegant. Have you got any idea why it isn't executing properly? There's roughly 3,000 rows in my source table, if that's relevant.

 

I can't post the results as my file contains sensitive information, but the issue is that the list does not expand to new columns, meaning the table has only the original "NAME" and "ID" columns. Thanks again!

@Insert_Key my bad, I forgot that Table.FromRecords takes resulting columns from the very first record in the list. Then lets create tables from each group of data and combine them. Try this code. 

 

let
    Source = Excel.CurrentWorkbook(){[Name="Table5"]}[Content],
    // function uses table column name to create a record of distinct values 
    fx = (tbl, col) => 
        [values = List.Distinct(Table.Column(tbl, col)),
        count = List.Count(values),
        names = if count = 1 then {col} else List.Transform({1..count}, (x) => col & " " & Text.From(x)),
        out = Record.FromList(values, names)][out],
    // column names - you may filter unwanted columns
    columns = List.Buffer(Table.ColumnNames(Source)),
    // control "group by" list, now it produces OUTPUT B from your sample. To get OUTPUT A use {"NAME"}
    to_rows = Table.Group(
        Source, 
        {"NAME", "ID"}, 
        {"x", (x) => Table.FromRecords(
            {Record.Combine(List.Transform(columns, (w) => fx(x, w)))}
        )}),
    to_table = Table.Combine(to_rows[x])
in
    to_table

 

Amazing - it works flawlessly. Thank you so much! 🏆

Sorry to be a PITA, @AlienSx, but I'm asking for little more help if possible, please.

When I got to the office this morning and had a look at the output using my real data, I realised that where a user had only a single Group associated with their Name or Name/ID (depending on which Option was used) the Group would be listed under a column named "Group" where if a User had multiple Groups, they would start populating across from "Group 1". Ideally they'd all follow the same structure, either starting at "Group" or "Group 1" and then a sequence "Group 2", "Group 3" and so on. I couldn't figure out how to adjust the code to start the sequence at from "2" rather than "1" so I did something inelegant and removed

if count = 1 then {col} else

from your fx step, and each column title now starts with a "1" appended - whether there are multiples or not. Like I said, inelegant... but it's definitely workable.

 

What I failed to understand yesterday is that the raw data that will be exported and presented to me on a regular basis has fairly meaningless headers/column titles that need modifying. Some contain carriage returns/line break. I have spent quite a bit of time Googling and trying to get my head around the various possible functions that could be used but haven't been able to change the headers without corrupting the output in one way or another. Please can give me some guidance on how to approach this either integrating into your solution or either an extra step(s) above below it? I've updated the sample file to contain a new table ("NEW_SOURCE") with some ugly headers for replacing with those in the other tables and my initial post.

I really appreciate your help! 😊

@Insert_Key 

regarding Group, Group 2 etc: try this: 

if count = 1 then {col} else {col} & List.Transform({2..count}, (x) => col & " " & Text.From(x))

regarding ugly headers: if columns always follow the same order then smth like this should work:

Table.RenameColumns(Source, List.Zip({Table.ColumnNames(Source), {"OWNER", "GROUP", "ID", "NAME", "LOG"}))

Helpful resources

Announcements
Las Vegas 2025

Join us at the Microsoft Fabric Community Conference

March 31 - April 2, 2025, in Las Vegas, Nevada. Use code MSCUST for a $150 discount! Prices go up Feb. 11th.

Jan25PBI_Carousel

Power BI Monthly Update - January 2025

Check out the January 2025 Power BI update to learn about new features in Reporting, Modeling, and Data Connectivity.

Jan NL Carousel

Fabric Community Update - January 2025

Find out what's new and trending in the Fabric community.