{
  "id": "P136",
  "slug": "attribution",
  "key": "P136-attribution",
  "title": "Attribution",
  "summary": "Conversions after paths of touches from four channels, credited by first touch, last touch, linear and position models with the same total and different leaders, and last touch decided by its lookback window.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P136 - Attribution\n\nConversions, each worth 60, each after a path of touches from search,\nsocial, email and affiliate, and four ways to share out the credit.\n\n- `main.eml` - the menu, the conversions, the four models side by side\n  with each channel's rank, last touch by lookback window, and adding or\n  removing conversions\n- `credit.eml` - the paths, each model's shares, the credit per channel,\n  last touch within a window, the leader and the ranks\n\nHow each part works:\n\n- A touch is a channel and the days before the conversion, listed from\n  the oldest. The models:\n  - first: all 60 to the first touch; last: all 60 to the last;\n  - linear: an equal share to every touch;\n  - position: 40% to the first and 40% to the last, 20% split among the\n    ones between (one touch takes all, two take half each).\n  Paths have 1 to 6 touches, so every share is a whole number.\n- As in the corpus case `last-touch-vs-first-touch`, every model gives out\n  exactly 60 a conversion: a check that the credit adds up to the\n  conversions passes for all four and says nothing about the split. For\n  the eight default conversions the leader is social under first, email\n  under last, and a tie of the two under linear and position.\n- As in the corpus case `attribution-window-picks-the-winner`, last touch\n  with a lookback window counts only touches within the window, and a\n  conversion with none goes to nobody. With a window of 1, 3 or 7 days\n  search leads; with 14 or 30, email does.\n\nWhat is checked: menu choices 1 to 6; a path of 1 to 6 touches written\nchannel:days, a known channel, days from 0 to 90 that only go down; at\nmost 12 conversions. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the conversions, the models and the windows;\n- affiliate:40 email:35 added, dropped at every window, then the models.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- paths: a cancel, tv:3, search alone, search:x, search:91, search:5\n  email:9 (days going up) and seven touches;\n- four conversions of search:1 to reach 12, and one more;\n- every conversion removed, then the conversions, models and windows of\n  none, and one more removal.\n\nBuilt on the verified corpus cases `last-touch-vs-first-touch` (four\nmodels with the same total and different winners) and\n`attribution-window-picks-the-winner` (one model, and the lookback window\ndecides).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P136 attribution: the conversions and their paths, four models side by\n# side, last touch by lookback window, and adding or removing conversions.\nimport credit\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef path_text(p):\n    \"\" => out\n    for t in p:\n        if out != \"\":\n            out + \" -> \" => out\n        out + t[0] + \" (\" + str(t[1]) + \"d)\" => out\n    return out\n\ndef show(paths):\n    \"Each conversion is worth 60; each touch is a channel and the days before the conversion.\" ^0\n    for k in [0:len(paths) - 1]:\n        (\"  \" + lpad(str(k + 1), 2) + \"  \" + path_text(paths[k])) ^0\n\ndef header(first):\n    \"  \" + pad(first, 10) => h\n    for ch in credit.channels():\n        h + lpad(ch, 10) => h\n    return h\n\ndef models(paths):\n    (header(\"model\") + \"     total  leader\") ^0\n    [] => cs\n    for m in [\"first\", \"last\", \"linear\", \"position\"]:\n        credit.credit(paths, m) => c\n        cs + [c] => cs\n        \"  \" + pad(m, 10) => line\n        0 => total\n        for ch in credit.channels():\n            line + lpad(str(c[ch]), 10) => line\n            total + c[ch] => total\n        (line + lpad(str(total), 10) + \"  \" + credit.leader(c)) ^0\n    \"Ranks, 1 for the most credit:\" ^0\n    \"  \" + pad(\"channel\", 10) => h\n    for m in [\"first\", \"last\", \"linear\", \"position\"]:\n        h + lpad(m, 10) => h\n    h ^0\n    for ch in credit.channels():\n        \"  \" + pad(ch, 10) => line\n        for c in cs:\n            line + lpad(str(credit.rank(c, ch)), 10) => line\n        line ^0\n    (\"Every model gives out \" + str(60 * len(paths)) + \", all there is: a check that the credit adds up to the conversions passes for all four, and says nothing about the split.\") ^0\n\ndef windows(paths):\n    \"Last touch, counting only touches within the lookback window:\" ^0\n    (header(\"window\") + \"  attributed  dropped  leader\") ^0\n    for w in [1, 3, 7, 14, 30]:\n        credit.last_in_window(paths, w) => r\n        \"  \" + pad(str(w) + \" days\", 10) => line\n        for ch in credit.channels():\n            line + lpad(str(r[0][ch]), 10) => line\n        \"-\" => lead\n        if r[1] > 0:\n            credit.leader(r[0]) => lead\n        (line + lpad(str(r[1]), 12) + lpad(str(r[2]), 9) + \"  \" + lead) ^0\n    \"A conversion with no touch inside the window goes to nobody; attributed plus dropped is every conversion at every window.\" ^0\n\ndef add(paths):\n    if len(paths) >= 12:\n        \"12 conversions is the most.\" ^0\n        return paths\n    \"A path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\" ^0\n    plain(input(\"path> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return paths\n    [] => p\n    \"\" => w\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                \"\" => ch\n                \"\" => d\n                False => colon\n                for x in w:\n                    if x == \":\" and not colon:\n                        True => colon\n                    elif colon:\n                        d + x => d\n                    else:\n                        ch + x => ch\n                False => known\n                for k in credit.channels():\n                    if k == ch:\n                        True => known\n                if not known:\n                    \"Each touch is search, social, email or affiliate, a colon, and the days.\" ^0\n                    return paths\n                if len(d) == 0 or len(d) > 2:\n                    \"Days are a whole number from 0 to 90.\" ^0\n                    return paths\n                for x in d:\n                    if x < \"0\" or x > \"9\":\n                        \"Days are a whole number from 0 to 90.\" ^0\n                        return paths\n                if int(d) > 90:\n                    \"Days are a whole number from 0 to 90.\" ^0\n                    return paths\n                p + [[ch, int(d)]] => p\n            \"\" => w\n        else:\n            w + c => w\n    if len(p) == 0 or len(p) > 6:\n        \"A path has 1 to 6 touches.\" ^0\n        return paths\n    for k in [1:len(p) - 1]:\n        if p[k][1] > p[k - 1][1]:\n            \"List the touches from the oldest: the days can only go down.\" ^0\n            return paths\n    (\"Added conversion \" + str(len(paths) + 1) + \": \" + path_text(p)) ^0\n    return paths + [p]\n\ncredit.default_paths() => paths\n\n\"== Attribution ==\" ^0\n\"Conversions after paths of touches from four channels, and four ways to share out the credit.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        if len(paths) == 0:\n            \"No conversions.\" ^0\n        else:\n            show(paths)\n    elif choice == \"2\":\n        if len(paths) == 0:\n            \"No conversions.\" ^0\n        else:\n            models(paths)\n    elif choice == \"3\":\n        if len(paths) == 0:\n            \"No conversions.\" ^0\n        else:\n            windows(paths)\n    elif choice == \"4\":\n        add(paths) => paths\n    elif choice == \"5\":\n        if len(paths) == 0:\n            \"No conversions.\" ^0\n        else:\n            paths[0:len(paths) - 1] => paths\n            (\"Removed the last conversion; \" + str(len(paths)) + \" left.\") ^0\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import credit\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef path_text(p):\n    out = \"\"\n    for t in p:\n        if out != \"\":\n            out = out + \" -> \"\n        out = out + t[0] + \" (\" + str(t[1]) + \"d)\"\n    return out\n\ndef show(paths):\n    print(\"Each conversion is worth 60; each touch is a channel and the days before the conversion.\")\n    for k in range(0, len(paths)):\n        print(\"  \" + lpad(str(k + 1), 2) + \"  \" + path_text(paths[k]))\n\ndef header(first):\n    h = \"  \" + pad(first, 10)\n    for ch in credit.channels():\n        h = h + lpad(ch, 10)\n    return h\n\ndef models(paths):\n    print(header(\"model\") + \"     total  leader\")\n    cs = []\n    for m in [\"first\", \"last\", \"linear\", \"position\"]:\n        c = credit.credit(paths, m)\n        cs = cs + [c]\n        line = \"  \" + pad(m, 10)\n        total = 0\n        for ch in credit.channels():\n            line = line + lpad(str(c[ch]), 10)\n            total = total + c[ch]\n        print(line + lpad(str(total), 10) + \"  \" + credit.leader(c))\n    print(\"Ranks, 1 for the most credit:\")\n    h = \"  \" + pad(\"channel\", 10)\n    for m in [\"first\", \"last\", \"linear\", \"position\"]:\n        h = h + lpad(m, 10)\n    print(h)\n    for ch in credit.channels():\n        line = \"  \" + pad(ch, 10)\n        for c in cs:\n            line = line + lpad(str(credit.rank(c, ch)), 10)\n        print(line)\n    print(\"Every model gives out \" + str(60 * len(paths)) + \", all there is: a check that the credit adds up to the conversions passes for all four, and says nothing about the split.\")\n\ndef windows(paths):\n    print(\"Last touch, counting only touches within the lookback window:\")\n    print(header(\"window\") + \"  attributed  dropped  leader\")\n    for w in [1, 3, 7, 14, 30]:\n        r = credit.last_in_window(paths, w)\n        line = \"  \" + pad(str(w) + \" days\", 10)\n        for ch in credit.channels():\n            line = line + lpad(str(r[0][ch]), 10)\n        lead = \"-\"\n        if r[1] > 0:\n            lead = credit.leader(r[0])\n        print(line + lpad(str(r[1]), 12) + lpad(str(r[2]), 9) + \"  \" + lead)\n    print(\"A conversion with no touch inside the window goes to nobody; attributed plus dropped is every conversion at every window.\")\n\ndef add(paths):\n    if len(paths) >= 12:\n        print(\"12 conversions is the most.\")\n        return paths\n    print(\"A path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\")\n    t = plain(input(\"path> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return paths\n    p = []\n    w = \"\"\n    for c in t + \" \":\n        if c == \" \":\n            if w != \"\":\n                ch = \"\"\n                d = \"\"\n                colon = False\n                for x in w:\n                    if x == \":\" and not colon:\n                        colon = True\n                    elif colon:\n                        d = d + x\n                    else:\n                        ch = ch + x\n                known = False\n                for k in credit.channels():\n                    if k == ch:\n                        known = True\n                if not known:\n                    print(\"Each touch is search, social, email or affiliate, a colon, and the days.\")\n                    return paths\n                if len(d) == 0 or len(d) > 2:\n                    print(\"Days are a whole number from 0 to 90.\")\n                    return paths\n                for x in d:\n                    if x < \"0\" or x > \"9\":\n                        print(\"Days are a whole number from 0 to 90.\")\n                        return paths\n                if int(d) > 90:\n                    print(\"Days are a whole number from 0 to 90.\")\n                    return paths\n                p = p + [[ch, int(d)]]\n            w = \"\"\n        else:\n            w = w + c\n    if len(p) == 0 or len(p) > 6:\n        print(\"A path has 1 to 6 touches.\")\n        return paths\n    for k in range(1, len(p)):\n        if p[k][1] > p[k - 1][1]:\n            print(\"List the touches from the oldest: the days can only go down.\")\n            return paths\n    print(\"Added conversion \" + str(len(paths) + 1) + \": \" + path_text(p))\n    return paths + [p]\n\npaths = credit.default_paths()\nprint(\"== Attribution ==\")\nprint(\"Conversions after paths of touches from four channels, and four ways to share out the credit.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        if len(paths) == 0:\n            print(\"No conversions.\")\n        else:\n            show(paths)\n    elif choice == \"2\":\n        if len(paths) == 0:\n            print(\"No conversions.\")\n        else:\n            models(paths)\n    elif choice == \"3\":\n        if len(paths) == 0:\n            print(\"No conversions.\")\n        else:\n            windows(paths)\n    elif choice == \"4\":\n        paths = add(paths)\n    elif choice == \"5\":\n        if len(paths) == 0:\n            print(\"No conversions.\")\n        else:\n            paths = paths[0:len(paths) - 1]\n            print(\"Removed the last conversion; \" + str(len(paths)) + \" left.\")\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "credit.eml",
      "eml": "# P136 attribution - conversions, each worth 60, each after a path of\n# touches from marketing channels, and four ways to give each channel its\n# share. As in the corpus case last-touch-vs-first-touch, every model hands\n# out exactly the same total and the winner still changes. As in\n# attribution-window-picks-the-winner, last touch with a lookback window\n# drops the touches older than the window, and the window alone can pick\n# the winner.\n#\n# A touch is [channel, days before the conversion]; a path lists its\n# touches from the oldest. Paths have 1 to 6 touches, so every share of 60\n# is a whole number.\n\ndef channels():\n    return [\"search\", \"social\", \"email\", \"affiliate\"]\n\ndef default_paths():\n    return [[[\"social\", 20], [\"search\", 1]], [[\"social\", 15], [\"email\", 6]], [[\"affiliate\", 12], [\"email\", 9]], [[\"social\", 25], [\"email\", 12]], [[\"search\", 3]], [[\"social\", 30], [\"affiliate\", 10], [\"email\", 13]], [[\"affiliate\", 8], [\"social\", 3]], [[\"email\", 18], [\"search\", 2]]]\n\ndef shares(path, model):\n    # [channel, share of 60] for each touch of the path\n    len(path) => n\n    [] => out\n    for k in [0:n - 1]:\n        0 => s\n        if model == \"first\":\n            if k == 0:\n                60 => s\n        elif model == \"last\":\n            if k == n - 1:\n                60 => s\n        elif model == \"linear\":\n            int(60 / n) => s\n        else:\n            # position: 40% to the first touch, 40% to the last, 20% split\n            # among the ones between; one touch takes all, two take half each\n            if n == 1:\n                60 => s\n            elif n == 2:\n                30 => s\n            elif k == 0 or k == n - 1:\n                24 => s\n            else:\n                int(12 / (n - 2)) => s\n        out + [[path[k][0], s]] => out\n    return out\n\ndef credit(paths, model):\n    # {channel: credit}\n    {} => c\n    for ch in channels():\n        0 => c[ch]\n    for p in paths:\n        for t in shares(p, model):\n            c[t[0]] + t[1] => c[t[0]]\n    return c\n\ndef last_in_window(paths, window):\n    # [{channel: credit}, conversions attributed, dropped]: the last touch no\n    # more than window days before the conversion takes all 60\n    {} => c\n    for ch in channels():\n        0 => c[ch]\n    0 => got\n    0 => dropped\n    for p in paths:\n        \"\" => winner\n        for t in p:\n            if t[1] <= window:\n                t[0] => winner\n        if winner == \"\":\n            dropped + 1 => dropped\n        else:\n            c[winner] + 60 => c[winner]\n            got + 1 => got\n    return [c, got, dropped]\n\ndef leader(c):\n    # the channels with the most credit, joined by \"and\" when they tie\n    0 - 1 => most\n    for ch in channels():\n        if c[ch] > most:\n            c[ch] => most\n    \"\" => out\n    for ch in channels():\n        if c[ch] == most:\n            if out != \"\":\n                out + \" and \" => out\n            out + ch => out\n    return out\n\ndef rank(c, ch):\n    1 => r\n    for other in channels():\n        if c[other] > c[ch]:\n            r + 1 => r\n    return r\n",
      "python": "def channels():\n    return [\"search\", \"social\", \"email\", \"affiliate\"]\n\ndef default_paths():\n    return [[[\"social\", 20], [\"search\", 1]], [[\"social\", 15], [\"email\", 6]], [[\"affiliate\", 12], [\"email\", 9]], [[\"social\", 25], [\"email\", 12]], [[\"search\", 3]], [[\"social\", 30], [\"affiliate\", 10], [\"email\", 13]], [[\"affiliate\", 8], [\"social\", 3]], [[\"email\", 18], [\"search\", 2]]]\n\ndef shares(path, model):\n    n = len(path)\n    out = []\n    for k in range(0, n):\n        s = 0\n        if model == \"first\":\n            if k == 0:\n                s = 60\n        elif model == \"last\":\n            if k == n - 1:\n                s = 60\n        elif model == \"linear\":\n            s = int(60 / n)\n        elif n == 1:\n            s = 60\n        elif n == 2:\n            s = 30\n        elif k == 0 or k == n - 1:\n            s = 24\n        else:\n            s = int(12 / (n - 2))\n        out = out + [[path[k][0], s]]\n    return out\n\ndef credit(paths, model):\n    c = {}\n    for ch in channels():\n        c[ch] = 0\n    for p in paths:\n        for t in shares(p, model):\n            c[t[0]] = c[t[0]] + t[1]\n    return c\n\ndef last_in_window(paths, window):\n    c = {}\n    for ch in channels():\n        c[ch] = 0\n    got = 0\n    dropped = 0\n    for p in paths:\n        winner = \"\"\n        for t in p:\n            if t[1] <= window:\n                winner = t[0]\n        if winner == \"\":\n            dropped = dropped + 1\n        else:\n            c[winner] = c[winner] + 60\n            got = got + 1\n    return [c, got, dropped]\n\ndef leader(c):\n    most = 0 - 1\n    for ch in channels():\n        if c[ch] > most:\n            most = c[ch]\n    out = \"\"\n    for ch in channels():\n        if c[ch] == most:\n            if out != \"\":\n                out = out + \" and \"\n            out = out + ch\n    return out\n\ndef rank(c, ch):\n    r = 1\n    for other in channels():\n        if c[other] > c[ch]:\n            r = r + 1\n    return r\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n4\n\n4\ntv:3\n4\nsearch\n4\nsearch:x\n4\nsearch:91\n4\nsearch:5 email:9\n4\nsearch:9 search:8 search:7 search:6 search:5 search:4 search:3\n4\nsearch:1\n4\nsearch:1\n4\nsearch:1\n4\nsearch:1\n4\n5\n5\n5\n5\n5\n5\n5\n5\n5\n5\n5\n5\n1\n2\n3\n5\n6\n",
      "screen": "== Attribution ==\nConversions after paths of touches from four channels, and four ways to share out the credit.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> \nCancelled.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> tv:3\nEach touch is search, social, email or affiliate, a colon, and the days.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> search\nDays are a whole number from 0 to 90.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> search:x\nDays are a whole number from 0 to 90.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> search:91\nDays are a whole number from 0 to 90.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> search:5 email:9\nList the touches from the oldest: the days can only go down.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> search:9 search:8 search:7 search:6 search:5 search:4 search:3\nA path has 1 to 6 touches.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> search:1\nAdded conversion 9: search (1d)\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> search:1\nAdded conversion 10: search (1d)\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> search:1\nAdded conversion 11: search (1d)\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> search:1\nAdded conversion 12: search (1d)\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\n12 conversions is the most.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 11 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 10 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 9 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 8 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 7 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 6 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 5 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 4 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 3 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 2 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 1 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nRemoved the last conversion; 0 left.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 1\nNo conversions.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 2\nNo conversions.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 3\nNo conversions.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 5\nNo conversions.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\naffiliate:40 email:35\n3\n2\n6\n",
      "screen": "== Attribution ==\nConversions after paths of touches from four channels, and four ways to share out the credit.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 1\nEach conversion is worth 60; each touch is a channel and the days before the conversion.\n   1  social (20d) -> search (1d)\n   2  social (15d) -> email (6d)\n   3  affiliate (12d) -> email (9d)\n   4  social (25d) -> email (12d)\n   5  search (3d)\n   6  social (30d) -> affiliate (10d) -> email (13d)\n   7  affiliate (8d) -> social (3d)\n   8  email (18d) -> search (2d)\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 2\n  model         search    social     email affiliate     total  leader\n  first             60       240        60       120       480  social\n  last             180        60       240         0       480  email\n  linear           120       140       140        80       480  social and email\n  position         120       144       144        72       480  social and email\nRanks, 1 for the most credit:\n  channel        first      last    linear  position\n  search             3         2         3         3\n  social             1         3         1         1\n  email              3         1         1         1\n  affiliate          2         4         4         4\nEvery model gives out 480, all there is: a check that the credit adds up to the conversions passes for all four, and says nothing about the split.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 3\nLast touch, counting only touches within the lookback window:\n  window        search    social     email affiliate  attributed  dropped  leader\n  1 days            60         0         0         0           1        7  search\n  3 days           180        60         0         0           4        4  search\n  7 days           180        60        60         0           5        3  search\n  14 days          180        60       240         0           8        0  email\n  30 days          180        60       240         0           8        0  email\nA conversion with no touch inside the window goes to nobody; attributed plus dropped is every conversion at every window.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 4\nA path from the oldest touch: channel:days, such as social:20 email:5 search:1 (1 to 6 touches, days from 90 down to 0).\npath> affiliate:40 email:35\nAdded conversion 9: affiliate (40d) -> email (35d)\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 3\nLast touch, counting only touches within the lookback window:\n  window        search    social     email affiliate  attributed  dropped  leader\n  1 days            60         0         0         0           1        8  search\n  3 days           180        60         0         0           4        5  search\n  7 days           180        60        60         0           5        4  search\n  14 days          180        60       240         0           8        1  email\n  30 days          180        60       240         0           8        1  email\nA conversion with no touch inside the window goes to nobody; attributed plus dropped is every conversion at every window.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 2\n  model         search    social     email affiliate     total  leader\n  first             60       240        60       180       540  social\n  last             180        60       300         0       540  email\n  linear           120       140       170       110       540  email\n  position         120       144       174       102       540  email\nRanks, 1 for the most credit:\n  channel        first      last    linear  position\n  search             3         2         3         3\n  social             1         3         2         2\n  email              3         1         1         1\n  affiliate          2         4         4         4\nEvery model gives out 540, all there is: a check that the credit adds up to the conversions passes for all four, and says nothing about the split.\n\n1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "last-touch-vs-first-touch",
      "caseId": "315-last-touch-vs-first-touch",
      "title": "Last touch vs first touch — four models, different winners, identical totals"
    },
    {
      "slug": "attribution-window-picks-the-winner",
      "caseId": "308-attribution-window-picks-the-winner",
      "title": "Attribution window picks the winner — same model, same data, decided by a number in a config file"
    }
  ],
  "updated": "2026-10-17"
}
