# The Portfolio

> Count every bed under a name. The largest holdings rise first.

Canonical URL: <https://datadriven.io/problems/the_portfolio>

Domain: Python · Difficulty: medium · Seniority: L3

## Problem

A short-term rental marketplace exports its `listings`, one row per listing with a host and a bed count, and a single host can own several. We're publishing host standings, where a host's standing is the total beds across all of their listings. Produce the standings, biggest hosts first.

## Worked solution and explanation

### What this really is

Strip away the marketplace framing and this is a two-phase transform wearing a one-line 'rank the hosts' costume. Phase one collapses many listings into one row per host by summing beds; phase two walks those totals in order handing out positions. Anyone can sort a list. The first trap is that a host owns several listings, so positioning the rows as they arrive positions listings, not hosts, and a host with three small rooms leapfrogs a host with one big suite. The second trap: when two hosts tie, arbitrary input order decides who lands first unless you force `host_name` as the tie-break, so re-shuffling the input silently changes who sits at position 1.

---

### Break it down

#### Step 1: Aggregate total beds per host

Fold the listings into a dict keyed by host_id, carrying the host_name and a running bed total. This is the step that makes the whole problem about hosts instead of rows.

#### Step 2: Sort by total, then break ties by name

Sort the aggregated hosts by total beds descending, then host_name ascending. The secondary key is not decoration: without it, two tied hosts come out in whatever order the dict happened to yield, and your output stops being deterministic.

#### Step 3: Assign no-gap positions

Walk the sorted hosts once, tracking the previous total. Bump the position only when the current total differs from the previous one. Equal totals reuse the current position, and the next distinct total gets the very next number, never skipping.

---

### The solution

**Aggregate, sort, and assign positions**

```python
def rank_hosts(listings: list) -> list:
    totals = {}
    for row in listings:
        hid = row['host_id']
        if hid not in totals:
            totals[hid] = {'host_name': row['host_name'], 'total_beds': 0}
        totals[hid]['total_beds'] += row['beds']

    ordered = sorted(
        totals.values(),
        key=lambda h: (-h['total_beds'], h['host_name']),
    )

    result = []
    rank = 0
    prev_total = None
    for host in ordered:
        if host['total_beds'] != prev_total:
            rank += 1
            prev_total = host['total_beds']
        result.append({
            'host_name': host['host_name'],
            'total_beds': host['total_beds'],
            'rank': rank,
        })
    return result
```

> **Time and Space Complexity**
>
> **Time:** O(n + k log k) where n is the number of listings and k is the number of unique hosts. One pass to aggregate, one sort of the k hosts, one pass to position.
> 
> **Space:** O(k) for the aggregated dict and the output list.

> **Interviewers watch for**
>
> The tell is the tie-break. A candidate who sorts on total alone passes the visible example by luck of insertion order, then fails the moment the grader feeds the same hosts in a different order. Naming `host_name` as the secondary key without being told to is the seniority signal here.

> **Common pitfall**
>
> Positioning by listing instead of by host. If you increment a counter per row you rank individual listings, and a prolific host with many small rooms buries a host with one large one. Aggregate to host grain first, then position.

> **The trick**
>
> The no-gap rule is the whole difference between this and a running index. Compare against the previous total, not the previous index: increment only on a change, and ties collapse onto one number so the sequence after a tie continues instead of jumping.

---

## Common follow-up questions

- How would you express this in SQL? _(Tests SUM(beds) GROUP BY host_id with DENSE_RANK() OVER (ORDER BY SUM(beds) DESC).)_
- How would the output change if the next distinct total should skip numbers equal to the size of the tie instead of continuing consecutively? _(Tests understanding of gap behavior across the ranking family.)_
- How would you compute this over a billion listings that arrive as a stream? _(Tests scaling the aggregation when the listings will not fit in memory.)_

## Related

- [All practice problems](https://datadriven.io/problems)
- [Mock interview mode](https://datadriven.io/interview/the_portfolio)
- [Python Interview Questions](https://datadriven.io/python-interview-questions)
- [Data Engineering Interview Prep Guide](https://datadriven.io/data-engineer-interview-prep)
- [Daily Challenge](https://datadriven.io/daily)

---

Source: DataDriven (https://datadriven.io). DataDriven is the data engineering interview community. Live code execution in SQL, Python, and Spark sandboxes. Every feature is open to every member.