Home » Custom Grouping! Part 13

Custom Grouping! Part 13

Solving Custom Grouping Part 13 challenge by Power Query, Power BI, Excel, Python and R

Based on monthly transactions, categorize each customer for each month into one of the following groups: New: A customer who has never made a purchase in previous months. Active: A customer who made a purchase this month and in the previous month. Inactive: A customer who did not make any purchase this month. Returning: A customer who made a purchase this month but did not make a purchase in the previous month.

📌 Challenge Details and Links
Challenge Number: 165
Challenge Difficulty: ⭐⭐⭐
📥Download Sample File
📥Link to the solutions on LinkedIn

Solving the challenge of Custom Grouping! Part 13 with Power Query

Power Query solution 1 for Custom Grouping! Part 13, proposed by Alejandro Simón 🇵🇦 🇪🇸:
let
Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
Group = Table.Group(Source, {"Month"}, {{"H", each [Customer]}}),
LG = Table.Combine(List.Skip(List.Generate(()=> [x = 0, In = {}],
 each [x]
Power Query solution 2 for Custom Grouping! Part 13, proposed by Alexis Olson:
let
  Source = Table.TransformColumnTypes(
    Excel.CurrentWorkbook(){[Name = "Table1"]}[Content], 
    {{"Month", Int64.Type}, {"Customer", type text}, {"Quantity", Int64.Type}}
  ), 
  Months = Table.Skip(Table.Distinct(Table.SelectColumns(Source, {"Month"}))), 
  Customers = List.Distinct(Source[Customer]), 
  CrossJoin = Table.ExpandListColumn(
    Table.AddColumn(Months, "Customer", each Customers), 
    "Customer"
  ), 
  Merge = Table.NestedJoin(
    CrossJoin, 
    {"Customer"}, 
    Source, 
    {"Customer"}, 
    "Source", 
    JoinKind.LeftOuter
  ), 
  AddCols = Table.AddColumn(
    Merge, 
    "Cols", 
    each [
      L         = [Source][Month], 
      Match     = List.Contains(L, [Month]), 
      New       = [Month] = List.Min(L), 
      Active    = List.ContainsAll(L, {[Month], [Month] - 1}), 
      Inactive  = not Match and List.Min(L) <= [Month], 
      Returning = Match and New = Active
    ]
  ), 
  Cols = {"New", "Active", "Inactive", "Returning"}, 
  ExpandCols = Table.ExpandRecordColumn(AddCols, "Cols", Cols), 
  f = (T, col) => Text.Combine(Table.SelectRows(T, each Table.Column(_, col))[Customer], ", "), 
  GroupRows = Table.Group(
    ExpandCols, 
    {"Month"}, 
    List.Transform(Cols, (col) => {col, each f(_, col)})
  )
in
  GroupRows
Power Query solution 3 for Custom Grouping! Part 13, proposed by Kris Jaganah:
let
  A = Excel.CurrentWorkbook(){[Name = "Table1"]}[Content][[Month], [Customer]], 
  B = Table.AddColumn(
    A, 
    "Clas", 
    each 
      let
        a = List.Max(
          Table.SelectRows(A, (x) => x[Customer] = [Customer] and x[Month] < [Month])[Month], 
          0
        ), 
        b = if [Month] - a = 1 then "Active" else if a = 0 then "New" else "Returning"
      in
        b
  ), 
  C = Table.Pivot(B, List.Distinct(B[Clas]), "Clas", "Customer", each Text.Combine(_, ", ")), 
  D = Table.AddColumn(
    C, 
    "Inactive", 
    each 
      let
        a = List.Distinct(Table.SelectRows(A, (y) => y[Month] < [Month])[Customer]), 
        b = Text.Split(Text.Combine(List.Skip(Record.ToList(_)), ", "), ", "), 
        c = Text.Combine(List.Difference(a, b), ", ")
      in
        c
  ), 
  E = Table.SelectColumns(D, {"Month", "New", "Active", "Inactive", "Returning"}), 
  F = Table.SelectRows(E, each ([Month] <> 1))
in
  F
Power Query solution 4 for Custom Grouping! Part 13, proposed by Seokho MOON:
let
  Source = Excel.CurrentWorkbook(){[Name = "Table1"]}[Content], 
  Group = Table.Group(Source, {"Month"}, {"V", each [Customer]}), 
  Recs = List.Generate(
    () => [N = 0, V = Group[V]{0}, A = V], 
    each [N] < Table.RowCount(Group), 
    each [
      N         = [N] + 1, 
      V         = Group[V]{N}, 
      A         = List.Distinct([A] & V), 
      Month     = Group[Month]{N}, 
      New       = List.Difference(V, [A]), 
      Active    = List.Intersect({V, [V]}), 
      Inactive  = List.Difference(A, V), 
      Returning = List.Difference(V, New & Active)
    ], 
    each [[Month], [New], [Active], [Inactive], [Returning]]?
  ), 
  Res = Table.TransformColumns(
    Table.Skip(Table.FromRecords(Recs), 1), 
    {"Month", each _}, 
    each Text.Combine(_, ", ")
  )
in
  Res
Power Query solution 5 for Custom Grouping! Part 13, proposed by Alexandre Garcia:
let
A = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
B = List.Difference,
C = (x)=> Text.Combine(x, ", "),
D = Table.Group(A, "Month", {"x", each [Customer]}),
E = List.Accumulate(List.RemoveLastN(D[Month]), {}, (s,c) => let a = List.Distinct(List.Combine(List.FirstN(D[x],c))), b = D[x]{c}, d = D[x]{c-1}
in s & {c+1, C(B(b, a)), C(List.Intersect({d, b})), C(B(a, b)), C(List.RemoveItems(List.RemoveItems(b , d), B(b, a)))}),
F = Table.FromRows(List.Split(E,5), {"Month", "New", "Active", "Inactive", "Returning"})
in F
Power Query solution 6 for Custom Grouping! Part 13, proposed by Glyn Willis:
let
  Months = List.Buffer(List.Distinct(CT[Month])), 
  Source = Excel.CurrentWorkbook(){[Name = "Table1"]}[Content], 
  CT = Table.TransformColumnTypes(
    Source, 
    {{"Month", Int64.Type}, {"Customer", type text}, {"Quantity", Int64.Type}}
  ), 
  Rec = Table.FromRecords(
    List.Combine(
      Table.Group(
        CT, 
        {"Customer"}, 
        {
          {
            "A", 
            each [
              tbl = Table.Buffer(_), 
              item = List.First(tbl[Customer]), 
              result = List.Transform(
                Months, 
                (r) =>
                  [
                    fP = List.Min(allM), 
                    allM = List.Buffer(tbl[Month]), 
                    Month = r, 
                    N = if fP <> 1 and r = fP then item else null, 
                    A = 
                      if r
                        > fP
                          and List.Count(List.PositionOfAny(allM, {r, r - 1}, Occurrence.All))
                          = 2
                      then
                        item
                      else
                        null, 
                    I = if r > fP and List.PositionOf(allM, r) = - 1 then item else null, 
                    R = 
                      if r
                        <> fP and List.PositionOf(allM, r)
                        <> - 1 and List.PositionOf(allM, r - 1)
                        = - 1
                      then
                        item
                      else
                        null
                  ][[Month], [N], [A], [I], [R]]
              )
            ][result], 
            type table
          }
        }
      )[A]
    )
  ), 
  Group = Table.Group(
    Rec, 
    {"Month"}, 
    {
      {"New", each Text.Combine([N], ", "), type nullable text}, 
      {"Active", each Text.Combine([A], ", "), type nullable text}, 
      {"Inacticve", each Text.Combine([I], ", "), type nullable text}, 
      {"Returning", each Text.Combine([R], ", "), type nullable text}
    }
  )
in
  Group
Power Query solution 7 for Custom Grouping! Part 13, proposed by Vida Vaitkunaite:
let
 A = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
 B = Table.RenameColumns(Table.FromList(List.Distinct(A[Month]), Splitter.SplitByNothing()), {"Column1", "Month"}),
 C = Table.AddColumn(B, "Custom", each let
a = Table.RemoveColumns(A, "Quantity"),
b = Table.SelectRows(a, (x)=> x[Month]<=[Month]),
c = Table.Group(b, {"Customer"}, {{"Count", each Table.RowCount(_)}}),
d = Table.RemoveColumns(Table.RenameColumns(Table.SelectRows(a, (x)=> x[Month]=[Month]), {"Customer", "Cust"}), {"Month"}),
e = Table.Join(c, {"Customer"}, d, {"Cust"},1),
f = Table.RemoveColumns(Table.RenameColumns(Table.SelectRows(a, (x)=> x[Month]=[Month]-1), {"Customer", "Prev"}),{"Month"}),
g = Table.Join(e, {"Customer"}, f, {"Prev"},1),
h = Table.AddColumn(g, "Result", (x)=> if x[Count]=1 and x[Cust]<> null then "New" else if x[Count]>1 and x[Cust] <> null and x[Prev] <> null then "ACTIVE" else if x[Count]>=1 and x[Cust] = null then "Inactive" else "Returning"),
i = Table.Group(h, {"Result"}, {{"All", each Text.Combine(_[Customer], ", ")}})
in i),
 D = Table.ExpandTableColumn(C, "Custom", {"Result", "All"}, {"Result", "All"}),
 E = Table.SelectRows(Table.Pivot(D, List.Distinct(D[Result]), "Result", "All"), each ([Month] <> 1))
in
 E

Solving the challenge of Custom Grouping! Part 13 with Excel

Excel solution 1 for Custom Grouping! Part 13, proposed by Bo Rydobon 🇹🇭:
=LET(m,
    B3:B11,
    c,
    C3:C11,
    n,
    XLOOKUP(
        c,
        c,
        m
    )=m,
    y,
    ISNA(
        XMATCH(
            c&m-1,
            c&m
        )
    ),
    VSTACK({"Month",
    "New",
    "ACTIVE",
    "Inactive",
    "Returning"},DROP(GROUPBY(m,
    HSTACK(REPT(
        c,
        n
    ),
    REPT(
        c,
        1-y
    ),
    MAP(m,
    LAMBDA(i,
    ARRAYTOTEXT(UNIQUE(FILTER(c,
    (m
Excel solution 2 for Custom Grouping! Part 13, proposed by Kris Jaganah:
=LET(a,
    B3:B11,
    b,
    C3:C11,
    c,
    D3:D11,
    d,
    MAP(a,
    b,
    LAMBDA(x,
    y,
    MAX(FILTER(a,
    (b=y)*(a1
            ),
            i
        ),
        1,
        3,
        2,
        5,
        4
    ))
Excel solution 3 for Custom Grouping! Part 13, proposed by JvdV -:
=LET(
    m,
    B3:B11,
    c,
    C3:C11,
    y,
    UNIQUE(
        m
    ),
    x,
    TOROW(
        UNIQUE(
            c
        )
    ),
    f,
    COUNTIFS,
    z,
    TOCOL(
        DROP(
            y&"|"&x&"|"&IFS(
                f(
                    m,
                    "<="&y,
                    c,
                    x
                ),
                IF(
                    f(
                        m,
                        "<"&y,
                        c,
                        x
                    )=0,
                    "New",
                    IF(
                        f(
                            m,
                            y,
                            c,
                            x
                        ),
                        IF(
                            f(
                                m,
                                y-1,
                                c,
                                x
                            ),
                            "Active",
                            "Returning"
                        ),
                        "Inactive"
                    )
                )
            ),
            1
        ),
        2
    ),
    r,
    REGEXEXTRACT,
    CHOOSECOLS(
        PIVOTBY(
            r(
                z,
                "w+"
            ),
            r(
                z,
                "w+$"
            ),
            r(
                z,
                "|Kw+"
            ),
            ARRAYTOTEXT,
            ,
            0,
            ,
            0
        ),
        1,
        4,
        2,
        3,
        5
    )
)
Excel solution 4 for Custom Grouping! Part 13, proposed by Gabriel Pugliese:
=LET(m,
    AB3:AB11,
    c,
    AC3:AC11,
    q,
    AD3:AD11,mct,
    TOCOL(
        UNIQUE(
            m
        )&TOROW(
            UNIQUE(
                c
            )
        )
    ),mm,
    --LEFT(
        mct
    ),
    cc,
    RIGHT(
        mct
    ),qq,
    XLOOKUP(
        mct,
        m&c,
        q,
        0
    ),base,
    HSTACK(
        mct,
        mm,
        cc,
        qq
    ),fdate,
    MAP(INDEX(
        base,
        ,
        3
    ),
    LAMBDA(x,
    MIN(FILTER(INDEX(
        base,
        ,
        2
    ),
    (INDEX(
        base,
        ,
        4
    )>0)*(INDEX(
        base,
        ,
        3
    )=x))))),base2,
    SORT(
        HSTACK(
            base,
            fdate
        ),
        {3,
        2},
        {1,
        1}
    ),status,
    MAP(SEQUENCE(
        ROWS(
            base2
        )
    ),
    LAMBDA(i,
    IFS(INDEX(
        base2,
        i,
        2
    )=INDEX(
        base2,
        i,
        5
    ),
    "new",INDEX(
        base2,
        i,
        2
    )INDEX(
        base2,
        i,
        5
    ))*(INDEX(
        base2,
        i,
        4
    )=0),
    "inactive",(INDEX(
    base2,
    i-1,
    4
)=0)*(INDEX(
        base2,
        i,
        4
    )>0),
    "returning",(INDEX(
    base2,
    i-1,
    4
)>0)*(INDEX(
        base2,
        i,
        4
    )>0),
    "active"))),
    base3,
    HSTACK(
        base2,
        status
    ),base4,
    FILTER(base3,
    (INDEX(
        base3,
        ,
        6
    )>"")*(INDEX(
        base3,
        ,
        2
    )>1)),hh,
    SORTBY(
        TOROW(
            UNIQUE(
                INDEX(
                    base4,
                    ,
                    6
                )
            )
        ),
        {2,
        3,
        4,
        1}
    ),rr,
    REDUCE(hh,
    UNIQUE(
        INDEX(
            base4,
            ,
            2
        )
    ),
    LAMBDA(a,
    v,
    VSTACK(a,
    MAP(hh,
    LAMBDA(x,
    ARRAYTOTEXT(FILTER(INDEX(
        base4,
        ,
        3
    ),
    (INDEX(
                    base4,
                    ,
                    6
                )=x)*(INDEX(
            base4,
            ,
            2
        )=v),
    ""))))))),HSTACK(
    VSTACK(
        "Month",
        UNIQUE(
        INDEX(
            base4,
            ,
            2
        )
    )
    ),
    rr
))
Excel solution 5 for Custom Grouping! Part 13, proposed by Pieter de B.:
=LET(
    b,
    B3:B11,
    c,
    C3:C11,
    n,
    IF(
        XLOOKUP(
            c,
            c,
            b
        )=b,
        c,
        ""
    ),
    a,
    IF(
        ISNA(
            XMATCH(
                c&b-1,
                c&b
            )
        ),
        "",
        c
    ),
    i,
    IF(
        XMATCH(
            b,
            b
        )=ROW(
            b
        )-2,
        MAP(
            b,
            LAMBDA(
                m,
                LET(
                    y,
                    FILTER(
                        c,
                        b<=m,
                        ""
                    ),
                    ARRAYTOTEXT(
                        UNIQUE(
                            FILTER(
                                y,
                                ISNA(
                                    XMATCH(
                                        y,
                                        FILTER(
                                            c,
                                            b=m
                                        )
                                    )
                                ),
                                ""
                            )
                        )
                    )
                )
            )
        ),
        ""
    ),
    r,
    IF(
        LEN(
            n
        )+LEN(
            a
        ),
        "",
        c
    ),
    VSTACK(
        {"Month",
        "New",
        "Active",
        "Inactive",
        "Returning"},
        DROP(
            GROUPBY(
                b,
                HSTACK(
                    n,
                    a,
                    i,
                    r
                ),
                LAMBDA(
                    x,
                    TEXTJOIN(
                        ", ",
                        ,
                        x
                    )
                ),
                ,
                0
            ),
            1
        )
    )
)

Solving the challenge of Custom Grouping! Part 13 with Python

Python solution 1 for Custom Grouping! Part 13, proposed by Konrad Gryczan, PhD:
import pandas as pd
path = "CH-165 Customer Grouping.xlsx"
input = pd.read_excel(path, usecols="B:D", skiprows=1, nrows=10)
test = pd.read_excel(path, usecols="F:J", skiprows=1, nrows=4).rename(columns=lambda x: x.split('.')[0])
grid = input.set_index(['Month', 'Customer']).unstack(fill_value=0).stack().reset_index()
def get_status(row, first_date):
 if row['Month'] == first_date:
 return "New"
 if row['Quantity'] == 0:
 return "" if row['Month'] < first_date else "Inactive"
 return "Returning" if row['Quantity_lag'] == 0 else "ACTIVE"
grid['Quantity_lag'] = grid.groupby('Customer')['Quantity'].shift(1).fillna(0)
grid['FirstDate'] = grid.groupby('Customer')['Month'].transform(lambda x: x[grid['Quantity'] > 0].min())
grid['status'] = grid.apply(lambda row: get_status(row, row['FirstDate']), axis=1)
grid = grid[(grid['Month'] > 1) & (grid['status'] != "")]
grid = grid.groupby(['Month', 'status'])['Customer'].apply(', '.join).unstack().reset_index()
grid = grid[['Month', 'New', 'ACTIVE', 'Inactive', 'Returning']]
grid.columns.name = None
print(grid.equals(test)) # True

Solving the challenge of Custom Grouping! Part 13 with R

R solution 1 for Custom Grouping! Part 13, proposed by Konrad Gryczan, PhD:
library(tidyverse)
library(readxl)
path <- "files/CH-165 Customer Grouping.xlsx"
input <- read_excel(path, range = "B2:D11")
test <- read_excel(path, range = "F2:J6")
grid <- expand.grid(unique(input$Month), unique(input$Customer)) %>%
 left_join(input, by = c("Var1" = "Month", "Var2" = "Customer")) %>%
 rename(Month = Var1, Customer = Var2) %>%
 replace_na(list(Quantity = 0)) %>%
 arrange(Customer, Month) %>%
 mutate(FirstDate = min(Month[Quantity > 0]),
 status = case_when(
 Month == FirstDate ~ "New",
 Month < FirstDate & Quantity == 0 ~ "",
 Month != FirstDate & Quantity == 0 ~ "Inactive",
 Month != FirstDate & Quantity > 0 & lag(Quantity) == 0 ~ "Returning",
 Month != FirstDate & Quantity > 0 & lag(Quantity) > 0 ~ "ACTIVE",
 TRUE ~ ""
 ), .by = Customer) %>%
 filter(Month > 1, status != "") %>%
 select(Month, Customer, status) %>%
 summarise(Customer = str_c(Customer, collapse = ", "), .by = c(Month, status)) %>%
 pivot_wider(names_from = status, values_from = Customer) %>%
 relocate(New, .after = Month)
all.equal(grid, test, check.attributes = FALSE) 
#> [1] TRUE

Solving the challenge of Custom Grouping! Part 13 with Google Sheets

Google Sheets solution 1 for Custom Grouping! Part 13, proposed by Peter Krkos:
PowerQuery solution:
https://docs.google.com/spreadsheets/d/1zR5IZLz8OT76vhaPEHfsPrw8-RDKnLyyqS49IJjdhFk/edit?pli=1&gid=1592914483#gid=1592914483

Leave a Reply