> ## Documentation Index
> Fetch the complete documentation index at: https://docs.canary.effectiveai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Find a mileage tipping point

> Price households across a grid of annual mileages in a study, and find the mileage bands where each carrier is cheaper.

Find the annual mileages where the cheaper carrier changes, for households you define.

[Install Relativities and download the example data](/relativities/setup) before running this page.

<Accordion title="Set up the book and the two carriers">
  The book is the synthetic six-household book in `relativities-examples/ppa/book`. Carrier A
  rates liability and collision per vehicle for a six-month term. Carrier B rates an account for a
  twelve-month term and returns one premium. [Compare carriers](/relativities/how-to/compare-carriers) shows
  how to check what each carrier receives.

  ```python theme={null}
  from pathlib import Path

  import pandas as pd

  from relativities import Book, Market, Program, Rater, Step, Study, ppa

  book = Book.read_csv("relativities-examples/ppa/book", schema=ppa.SCHEMA)


  VEHICLE = ["submission_id", "vehicle_id"]


  def years(start, on):
      """Whole years from each start date to each on date."""
      before = on.dt.strftime("%m%d") < start.dt.strftime("%m%d")
      return on.dt.year - start.dt.year - before.astype(int)


  def on_effective_date(book):
      """Each driver with their submission's effective date."""
      return book.drivers.merge(book.submissions[["submission_id", "effective_date"]])


  def nest(book, **lists):
      """Each submission's input: one list of records per keyword, in book order."""
      grouped = {
          name: {sid: rows.drop(columns="submission_id").to_dict("records") for sid, rows in frame.groupby("submission_id", sort=False)}
          for name, frame in lists.items()
      }
      return {sid: {name: grouped[name].get(sid, []) for name in lists} for sid in book.submissions["submission_id"]}


  def carrier_a(book):
      drivers = on_effective_date(book)
      drivers["age"] = years(drivers["birth_date"], drivers["effective_date"])
      coverages = book.vehicle_coverages
      collision = coverages[coverages["coverage_id"] == "collision"][[*VEHICLE, "deductible"]]
      vehicles = book.vehicles.merge(collision, on=VEHICLE, how="left")
      vehicles["collision_deductible"] = vehicles["deductible"].fillna(0).astype(int)
      return nest(
          book,
          drivers=drivers[["submission_id", "age"]],
          vehicles=vehicles[["submission_id", "annual_mileage", "collision_deductible"]],
      )


  def carrier_b(book):
      drivers = on_effective_date(book)
      blank = drivers.loc[drivers["first_licensed_date"].isna(), "driver_id"]
      if len(blank):
          raise ValueError(f"driver {blank.iloc[0]} has no first_licensed_date")
      drivers["years_licensed"] = years(drivers["first_licensed_date"], drivers["effective_date"])
      coverages = book.vehicle_coverages
      comprehensive = coverages[coverages["coverage_id"] == "comprehensive"][VEHICLE]
      autos = book.vehicles.merge(comprehensive.assign(comprehensive=True), on=VEHICLE, how="left")
      autos["comprehensive"] = autos["comprehensive"].eq(True)
      autos = autos.rename(columns={"annual_mileage": "miles"})
      return nest(
          book,
          operators=drivers[["submission_id", "years_licensed"]],
          autos=autos[["submission_id", "miles", "comprehensive"]],
      )


  market = Market(
      {
          "a": Program(Rater.load(Path("relativities-examples/ppa/carrier-a.zip")), carrier_a, schema=ppa.SCHEMA),
          "b": Program(Rater.load(Path("relativities-examples/ppa/carrier-b.zip")), carrier_b, schema=ppa.SCHEMA),
      }
  )
  ```
</Accordion>

```mermaid theme={null}
flowchart LR
  book["Book: two households"] --> study["Study: one scenario per mileage"]
  study --> rate["market.rate"]
  rate --> annual["Annual premium per program"]
  annual --> bands["Mileage bands"]
```

## Define the households

Build a [`Book`](/relativities/reference/relativities#book) from a dict of DataFrames, one per table, with the PPA schema. These two households
differ in one fact: when their driver was first licensed. Both ask for liability alone.

```python theme={null}
ids = ["S-experienced", "S-newly-licensed"]
households = ["experienced", "newly_licensed"]
profiles = Book(
    {
        "households": pd.DataFrame({"household_id": households}),
        "submissions": pd.DataFrame({"submission_id": ids, "household_id": households, "effective_date": "2026-10-01"}),
        "drivers": pd.DataFrame(
            {
                "submission_id": ids,
                "driver_id": ["D-experienced", "D-newly-licensed"],
                "birth_date": "1986-01-15",
                "first_licensed_date": ["2002-01-15", "2025-06-01"],
            }
        ),
        "vehicles": pd.DataFrame({"submission_id": ids, "vehicle_id": ["V-experienced", "V-newly-licensed"]}),
        "vehicle_coverages": pd.DataFrame({"submission_id": ids, "vehicle_id": ["V-experienced", "V-newly-licensed"], "coverage_id": "liability"}),
    },
    schema=ppa.SCHEMA,
)
profiles  # Book(2 households, 2 submissions, 2 drivers, 2 vehicles)
```

Supply the keys, `submissions.household_id` and `submissions.effective_date`. The book adds each
other core column, typed and empty. Text dates become dates.

## Price a grid of mileages

Make one scenario per mileage, from 0 to 32,000 miles a year in steps of 500. Put the scenarios in
a [`Study`](/relativities/reference/relativities#study), and rate the study with the market.

```python theme={null}
def at(book, miles):
    return book.with_tables(vehicles=book.vehicles.assign(annual_mileage=miles))


grid = {f"miles_{miles}": miles for miles in range(500, 32_001, 500)}
scenarios = {name: Step(at, miles=miles) for name, miles in grid.items()}
study = Study(profiles, steps=[Step(at, miles=0)], scenarios=scenarios)
rated = market.rate(study)
rated.groupby(["program", "status"]).size()
# program  status
# a        invalid_inputs      8
#          succeeded         122
# b        refused            28
#          succeeded         102
# dtype: int64
```

The study's `steps` set each mileage to 0 first, so `baseline` is the first grid point. The market
prices each household in all 65 scenarios.

## Annualize the premiums

Put both premiums on a twelve-month term: double carrier A's six-month premium. Then put the two
programs side by side.

```python theme={null}
rated = rated.assign(
    annual_mileage=rated["scenario_id"].map({"baseline": 0, **grid}),
    annual=lambda frame: frame["premium"].mul(2).where(frame["program"] == "a", frame["annual_premium"])
)
annual = rated.pivot(index=["submission_id", "annual_mileage"], columns="program", values="annual")
annual.loc["S-newly-licensed"].loc[[8_000, 8_500]]
# program             a      b
# annual_mileage
# 8000            480.0  588.0
# 8500            600.0  588.0
```

Between 8,000 and 8,500 miles a year, the cheaper carrier for the newly licensed driver changes
from carrier A to carrier B.

## Find the bands

Label each grid point with the cheaper carrier. Then join adjacent points with the same label and
the same premiums into one band.

```python theme={null}
def cheaper(row):
    rated = [program for program in ("a", "b") if pd.notna(row[program])]
    if len(rated) < 2:
        return f"only {rated[0]} rates" if rated else "neither rates"
    return "a" if row["a"] < row["b"] else "b"


grid = annual.assign(cheaper=annual.apply(cheaper, axis=1)).reset_index()
same = grid[["cheaper", "a", "b"]].fillna(0)  # compare a missing premium as 0
grid["band"] = same.ne(same.groupby(grid["submission_id"]).shift()).any(axis=1).cumsum()
bands = grid.groupby("band").agg(
    submission_id=("submission_id", "first"),
    from_miles=("annual_mileage", "min"),
    to_miles=("annual_mileage", "max"),
    cheaper=("cheaper", "first"),
    a=("a", "first"),
    b=("b", "first"),
)
```

## Read the bands

```python theme={null}
bands
#          submission_id  from_miles  to_miles        cheaper      a      b
# band
# 1        S-experienced           0      8000              b  480.0  420.0
# 2        S-experienced        8500     10000              b  600.0  420.0
# 3        S-experienced       10500     25000              b  600.0  480.0
# 4        S-experienced       25500     30000   only a rates  600.0    NaN
# 5        S-experienced       30500     32000  neither rates    NaN    NaN
# 6     S-newly-licensed           0      8000              a  480.0  588.0
# 7     S-newly-licensed        8500     10000              b  600.0  588.0
# 8     S-newly-licensed       10500     25000              a  600.0  672.0
# 9     S-newly-licensed       25500     30000   only a rates  600.0    NaN
# 10    S-newly-licensed       30500     32000  neither rates    NaN    NaN
```

For the experienced driver, carrier B is cheaper at every mileage that both carriers rate. For
the newly licensed driver, the cheaper carrier changes twice:

* Carrier A is cheaper up to 8,000 miles a year.
* Carrier B is cheaper from 8,500 to 10,000 miles a year, by \$12 for a twelve-month term.
* Carrier A is cheaper again from 10,500 to 25,000 miles a year.

Above 25,000 miles a year, carrier B refuses. Above 30,000, both carriers leave the household
unrated. Report each of these bands as a result.

## Place a change exactly

The grid brackets each change between adjacent mileage values. Refine that interval to the mileage precision your rating inputs accept.
Report the grid resolution with the threshold.

## Close the raters

```python theme={null}
for program in market.programs.values():
    program.close()
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.