<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P136-attribution/ | updated: 2026-10-17 -->

# P136 Attribution

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.

EML-P project `projects/attribution` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/attribution` runs it and `eml project verify projects/attribution` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: last-touch-vs-first-touch (https://efficientnewlanguage.org/cases/315-last-touch-vs-first-touch/), attribution-window-picks-the-winner (https://efficientnewlanguage.org/cases/308-attribution-window-picks-the-winner/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
4

4
tv:3
4
search
4
search:x
4
search:91
4
search:5 email:9
4
search:9 search:8 search:7 search:6 search:5 search:4 search:3
4
search:1
4
search:1
4
search:1
4
search:1
4
5
5
5
5
5
5
5
5
5
5
5
5
1
2
3
5
6
```

Screen:

```text
== Attribution ==
Conversions after paths of touches from four channels, and four ways to share out the credit.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 0
Pick a number from 1 to 6.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> x
Pick a number from 1 to 6.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> 
Cancelled.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> tv:3
Each touch is search, social, email or affiliate, a colon, and the days.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> search
Days are a whole number from 0 to 90.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> search:x
Days are a whole number from 0 to 90.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> search:91
Days are a whole number from 0 to 90.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> search:5 email:9
List the touches from the oldest: the days can only go down.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> search:9 search:8 search:7 search:6 search:5 search:4 search:3
A path has 1 to 6 touches.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> search:1
Added conversion 9: search (1d)

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> search:1
Added conversion 10: search (1d)

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> search:1
Added conversion 11: search (1d)

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> search:1
Added conversion 12: search (1d)

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
12 conversions is the most.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 11 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 10 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 9 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 8 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 7 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 6 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 5 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 4 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 3 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 2 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 1 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
Removed the last conversion; 0 left.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 1
No conversions.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 2
No conversions.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 3
No conversions.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 5
No conversions.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
3
4
affiliate:40 email:35
3
2
6
```

Screen:

```text
== Attribution ==
Conversions after paths of touches from four channels, and four ways to share out the credit.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 1
Each conversion is worth 60; each touch is a channel and the days before the conversion.
   1  social (20d) -> search (1d)
   2  social (15d) -> email (6d)
   3  affiliate (12d) -> email (9d)
   4  social (25d) -> email (12d)
   5  search (3d)
   6  social (30d) -> affiliate (10d) -> email (13d)
   7  affiliate (8d) -> social (3d)
   8  email (18d) -> search (2d)

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 2
  model         search    social     email affiliate     total  leader
  first             60       240        60       120       480  social
  last             180        60       240         0       480  email
  linear           120       140       140        80       480  social and email
  position         120       144       144        72       480  social and email
Ranks, 1 for the most credit:
  channel        first      last    linear  position
  search             3         2         3         3
  social             1         3         1         1
  email              3         1         1         1
  affiliate          2         4         4         4
Every 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.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 3
Last touch, counting only touches within the lookback window:
  window        search    social     email affiliate  attributed  dropped  leader
  1 days            60         0         0         0           1        7  search
  3 days           180        60         0         0           4        4  search
  7 days           180        60        60         0           5        3  search
  14 days          180        60       240         0           8        0  email
  30 days          180        60       240         0           8        0  email
A conversion with no touch inside the window goes to nobody; attributed plus dropped is every conversion at every window.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 4
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).
path> affiliate:40 email:35
Added conversion 9: affiliate (40d) -> email (35d)

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 3
Last touch, counting only touches within the lookback window:
  window        search    social     email affiliate  attributed  dropped  leader
  1 days            60         0         0         0           1        8  search
  3 days           180        60         0         0           4        5  search
  7 days           180        60        60         0           5        4  search
  14 days          180        60       240         0           8        1  email
  30 days          180        60       240         0           8        1  email
A conversion with no touch inside the window goes to nobody; attributed plus dropped is every conversion at every window.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 2
  model         search    social     email affiliate     total  leader
  first             60       240        60       180       540  social
  last             180        60       300         0       540  email
  linear           120       140       170       110       540  email
  position         120       144       174       102       540  email
Ranks, 1 for the most credit:
  channel        first      last    linear  position
  search             3         2         3         3
  social             1         3         2         2
  email              3         1         1         1
  affiliate          2         4         4         4
Every 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.

1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P136 attribution: the conversions and their paths, four models side by
# side, last touch by lookback window, and adding or removing conversions.
import credit

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def path_text(p):
    "" => out
    for t in p:
        if out != "":
            out + " -> " => out
        out + t[0] + " (" + str(t[1]) + "d)" => out
    return out

def show(paths):
    "Each conversion is worth 60; each touch is a channel and the days before the conversion." ^0
    for k in [0:len(paths) - 1]:
        ("  " + lpad(str(k + 1), 2) + "  " + path_text(paths[k])) ^0

def header(first):
    "  " + pad(first, 10) => h
    for ch in credit.channels():
        h + lpad(ch, 10) => h
    return h

def models(paths):
    (header("model") + "     total  leader") ^0
    [] => cs
    for m in ["first", "last", "linear", "position"]:
        credit.credit(paths, m) => c
        cs + [c] => cs
        "  " + pad(m, 10) => line
        0 => total
        for ch in credit.channels():
            line + lpad(str(c[ch]), 10) => line
            total + c[ch] => total
        (line + lpad(str(total), 10) + "  " + credit.leader(c)) ^0
    "Ranks, 1 for the most credit:" ^0
    "  " + pad("channel", 10) => h
    for m in ["first", "last", "linear", "position"]:
        h + lpad(m, 10) => h
    h ^0
    for ch in credit.channels():
        "  " + pad(ch, 10) => line
        for c in cs:
            line + lpad(str(credit.rank(c, ch)), 10) => line
        line ^0
    ("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

def windows(paths):
    "Last touch, counting only touches within the lookback window:" ^0
    (header("window") + "  attributed  dropped  leader") ^0
    for w in [1, 3, 7, 14, 30]:
        credit.last_in_window(paths, w) => r
        "  " + pad(str(w) + " days", 10) => line
        for ch in credit.channels():
            line + lpad(str(r[0][ch]), 10) => line
        "-" => lead
        if r[1] > 0:
            credit.leader(r[0]) => lead
        (line + lpad(str(r[1]), 12) + lpad(str(r[2]), 9) + "  " + lead) ^0
    "A conversion with no touch inside the window goes to nobody; attributed plus dropped is every conversion at every window." ^0

def add(paths):
    if len(paths) >= 12:
        "12 conversions is the most." ^0
        return paths
    "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
    plain(input("path> ")) => t
    if t == "":
        "Cancelled." ^0
        return paths
    [] => p
    "" => w
    for c in t + " ":
        if c == " ":
            if w != "":
                "" => ch
                "" => d
                False => colon
                for x in w:
                    if x == ":" and not colon:
                        True => colon
                    elif colon:
                        d + x => d
                    else:
                        ch + x => ch
                False => known
                for k in credit.channels():
                    if k == ch:
                        True => known
                if not known:
                    "Each touch is search, social, email or affiliate, a colon, and the days." ^0
                    return paths
                if len(d) == 0 or len(d) > 2:
                    "Days are a whole number from 0 to 90." ^0
                    return paths
                for x in d:
                    if x < "0" or x > "9":
                        "Days are a whole number from 0 to 90." ^0
                        return paths
                if int(d) > 90:
                    "Days are a whole number from 0 to 90." ^0
                    return paths
                p + [[ch, int(d)]] => p
            "" => w
        else:
            w + c => w
    if len(p) == 0 or len(p) > 6:
        "A path has 1 to 6 touches." ^0
        return paths
    for k in [1:len(p) - 1]:
        if p[k][1] > p[k - 1][1]:
            "List the touches from the oldest: the days can only go down." ^0
            return paths
    ("Added conversion " + str(len(paths) + 1) + ": " + path_text(p)) ^0
    return paths + [p]

credit.default_paths() => paths

"== Attribution ==" ^0
"Conversions after paths of touches from four channels, and four ways to share out the credit." ^0
True => running
while running:
    "" ^0
    "1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        if len(paths) == 0:
            "No conversions." ^0
        else:
            show(paths)
    elif choice == "2":
        if len(paths) == 0:
            "No conversions." ^0
        else:
            models(paths)
    elif choice == "3":
        if len(paths) == 0:
            "No conversions." ^0
        else:
            windows(paths)
    elif choice == "4":
        add(paths) => paths
    elif choice == "5":
        if len(paths) == 0:
            "No conversions." ^0
        else:
            paths[0:len(paths) - 1] => paths
            ("Removed the last conversion; " + str(len(paths)) + " left.") ^0
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import credit

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def path_text(p):
    out = ""
    for t in p:
        if out != "":
            out = out + " -> "
        out = out + t[0] + " (" + str(t[1]) + "d)"
    return out

def show(paths):
    print("Each conversion is worth 60; each touch is a channel and the days before the conversion.")
    for k in range(0, len(paths)):
        print("  " + lpad(str(k + 1), 2) + "  " + path_text(paths[k]))

def header(first):
    h = "  " + pad(first, 10)
    for ch in credit.channels():
        h = h + lpad(ch, 10)
    return h

def models(paths):
    print(header("model") + "     total  leader")
    cs = []
    for m in ["first", "last", "linear", "position"]:
        c = credit.credit(paths, m)
        cs = cs + [c]
        line = "  " + pad(m, 10)
        total = 0
        for ch in credit.channels():
            line = line + lpad(str(c[ch]), 10)
            total = total + c[ch]
        print(line + lpad(str(total), 10) + "  " + credit.leader(c))
    print("Ranks, 1 for the most credit:")
    h = "  " + pad("channel", 10)
    for m in ["first", "last", "linear", "position"]:
        h = h + lpad(m, 10)
    print(h)
    for ch in credit.channels():
        line = "  " + pad(ch, 10)
        for c in cs:
            line = line + lpad(str(credit.rank(c, ch)), 10)
        print(line)
    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.")

def windows(paths):
    print("Last touch, counting only touches within the lookback window:")
    print(header("window") + "  attributed  dropped  leader")
    for w in [1, 3, 7, 14, 30]:
        r = credit.last_in_window(paths, w)
        line = "  " + pad(str(w) + " days", 10)
        for ch in credit.channels():
            line = line + lpad(str(r[0][ch]), 10)
        lead = "-"
        if r[1] > 0:
            lead = credit.leader(r[0])
        print(line + lpad(str(r[1]), 12) + lpad(str(r[2]), 9) + "  " + lead)
    print("A conversion with no touch inside the window goes to nobody; attributed plus dropped is every conversion at every window.")

def add(paths):
    if len(paths) >= 12:
        print("12 conversions is the most.")
        return paths
    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).")
    t = plain(input("path> "))
    if t == "":
        print("Cancelled.")
        return paths
    p = []
    w = ""
    for c in t + " ":
        if c == " ":
            if w != "":
                ch = ""
                d = ""
                colon = False
                for x in w:
                    if x == ":" and not colon:
                        colon = True
                    elif colon:
                        d = d + x
                    else:
                        ch = ch + x
                known = False
                for k in credit.channels():
                    if k == ch:
                        known = True
                if not known:
                    print("Each touch is search, social, email or affiliate, a colon, and the days.")
                    return paths
                if len(d) == 0 or len(d) > 2:
                    print("Days are a whole number from 0 to 90.")
                    return paths
                for x in d:
                    if x < "0" or x > "9":
                        print("Days are a whole number from 0 to 90.")
                        return paths
                if int(d) > 90:
                    print("Days are a whole number from 0 to 90.")
                    return paths
                p = p + [[ch, int(d)]]
            w = ""
        else:
            w = w + c
    if len(p) == 0 or len(p) > 6:
        print("A path has 1 to 6 touches.")
        return paths
    for k in range(1, len(p)):
        if p[k][1] > p[k - 1][1]:
            print("List the touches from the oldest: the days can only go down.")
            return paths
    print("Added conversion " + str(len(paths) + 1) + ": " + path_text(p))
    return paths + [p]

paths = credit.default_paths()
print("== Attribution ==")
print("Conversions after paths of touches from four channels, and four ways to share out the credit.")
running = True
while running:
    print("")
    print("1) conversions  2) models  3) last touch by window  4) add a conversion  5) remove the last  6) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        if len(paths) == 0:
            print("No conversions.")
        else:
            show(paths)
    elif choice == "2":
        if len(paths) == 0:
            print("No conversions.")
        else:
            models(paths)
    elif choice == "3":
        if len(paths) == 0:
            print("No conversions.")
        else:
            windows(paths)
    elif choice == "4":
        paths = add(paths)
    elif choice == "5":
        if len(paths) == 0:
            print("No conversions.")
        else:
            paths = paths[0:len(paths) - 1]
            print("Removed the last conversion; " + str(len(paths)) + " left.")
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### credit.eml

```eml
# P136 attribution - conversions, each worth 60, each after a path of
# touches from marketing channels, and four ways to give each channel its
# share. As in the corpus case last-touch-vs-first-touch, every model hands
# out exactly the same total and the winner still changes. As in
# attribution-window-picks-the-winner, last touch with a lookback window
# drops the touches older than the window, and the window alone can pick
# the winner.
#
# A touch is [channel, days before the conversion]; a path lists its
# touches from the oldest. Paths have 1 to 6 touches, so every share of 60
# is a whole number.

def channels():
    return ["search", "social", "email", "affiliate"]

def default_paths():
    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]]]

def shares(path, model):
    # [channel, share of 60] for each touch of the path
    len(path) => n
    [] => out
    for k in [0:n - 1]:
        0 => s
        if model == "first":
            if k == 0:
                60 => s
        elif model == "last":
            if k == n - 1:
                60 => s
        elif model == "linear":
            int(60 / n) => s
        else:
            # position: 40% to the first touch, 40% to the last, 20% split
            # among the ones between; one touch takes all, two take half each
            if n == 1:
                60 => s
            elif n == 2:
                30 => s
            elif k == 0 or k == n - 1:
                24 => s
            else:
                int(12 / (n - 2)) => s
        out + [[path[k][0], s]] => out
    return out

def credit(paths, model):
    # {channel: credit}
    {} => c
    for ch in channels():
        0 => c[ch]
    for p in paths:
        for t in shares(p, model):
            c[t[0]] + t[1] => c[t[0]]
    return c

def last_in_window(paths, window):
    # [{channel: credit}, conversions attributed, dropped]: the last touch no
    # more than window days before the conversion takes all 60
    {} => c
    for ch in channels():
        0 => c[ch]
    0 => got
    0 => dropped
    for p in paths:
        "" => winner
        for t in p:
            if t[1] <= window:
                t[0] => winner
        if winner == "":
            dropped + 1 => dropped
        else:
            c[winner] + 60 => c[winner]
            got + 1 => got
    return [c, got, dropped]

def leader(c):
    # the channels with the most credit, joined by "and" when they tie
    0 - 1 => most
    for ch in channels():
        if c[ch] > most:
            c[ch] => most
    "" => out
    for ch in channels():
        if c[ch] == most:
            if out != "":
                out + " and " => out
            out + ch => out
    return out

def rank(c, ch):
    1 => r
    for other in channels():
        if c[other] > c[ch]:
            r + 1 => r
    return r
```

Python projection of credit.eml:

```python
def channels():
    return ["search", "social", "email", "affiliate"]

def default_paths():
    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]]]

def shares(path, model):
    n = len(path)
    out = []
    for k in range(0, n):
        s = 0
        if model == "first":
            if k == 0:
                s = 60
        elif model == "last":
            if k == n - 1:
                s = 60
        elif model == "linear":
            s = int(60 / n)
        elif n == 1:
            s = 60
        elif n == 2:
            s = 30
        elif k == 0 or k == n - 1:
            s = 24
        else:
            s = int(12 / (n - 2))
        out = out + [[path[k][0], s]]
    return out

def credit(paths, model):
    c = {}
    for ch in channels():
        c[ch] = 0
    for p in paths:
        for t in shares(p, model):
            c[t[0]] = c[t[0]] + t[1]
    return c

def last_in_window(paths, window):
    c = {}
    for ch in channels():
        c[ch] = 0
    got = 0
    dropped = 0
    for p in paths:
        winner = ""
        for t in p:
            if t[1] <= window:
                winner = t[0]
        if winner == "":
            dropped = dropped + 1
        else:
            c[winner] = c[winner] + 60
            got = got + 1
    return [c, got, dropped]

def leader(c):
    most = 0 - 1
    for ch in channels():
        if c[ch] > most:
            most = c[ch]
    out = ""
    for ch in channels():
        if c[ch] == most:
            if out != "":
                out = out + " and "
            out = out + ch
    return out

def rank(c, ch):
    r = 1
    for other in channels():
        if c[other] > c[ch]:
            r = r + 1
    return r
```

## README

# P136 - Attribution

Conversions, each worth 60, each after a path of touches from search,
social, email and affiliate, and four ways to share out the credit.

- `main.eml` - the menu, the conversions, the four models side by side
  with each channel's rank, last touch by lookback window, and adding or
  removing conversions
- `credit.eml` - the paths, each model's shares, the credit per channel,
  last touch within a window, the leader and the ranks

How each part works:

- A touch is a channel and the days before the conversion, listed from
  the oldest. The models:
  - first: all 60 to the first touch; last: all 60 to the last;
  - linear: an equal share to every touch;
  - position: 40% to the first and 40% to the last, 20% split among the
    ones between (one touch takes all, two take half each).
  Paths have 1 to 6 touches, so every share is a whole number.
- As in the corpus case `last-touch-vs-first-touch`, every model gives out
  exactly 60 a conversion: a check that the credit adds up to the
  conversions passes for all four and says nothing about the split. For
  the eight default conversions the leader is social under first, email
  under last, and a tie of the two under linear and position.
- As in the corpus case `attribution-window-picks-the-winner`, last touch
  with a lookback window counts only touches within the window, and a
  conversion with none goes to nobody. With a window of 1, 3 or 7 days
  search leads; with 14 or 30, email does.

What is checked: menu choices 1 to 6; a path of 1 to 6 touches written
channel:days, a known channel, days from 0 to 90 that only go down; at
most 12 conversions. An empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the conversions, the models and the windows;
- affiliate:40 email:35 added, dropped at every window, then the models.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- paths: a cancel, tv:3, search alone, search:x, search:91, search:5
  email:9 (days going up) and seven touches;
- four conversions of search:1 to reach 12, and one more;
- every conversion removed, then the conversions, models and windows of
  none, and one more removal.

Built on the verified corpus cases `last-touch-vs-first-touch` (four
models with the same total and different winners) and
`attribution-window-picks-the-winner` (one model, and the lookback window
decides).
