Causality APIs 11: The Tariff Moved The Shelf Price

When shelf prices jump after a tariff move, the tariff is an obvious suspect. The harder question is how much of the jump belongs to the tariff itself and how much belongs to the kinds of products that were exposed in the first place.

When a tariff lands and shelf prices jump, the argument usually skips straight to blame. Retailers point at the policy. Skeptics point at shipping costs, volatile categories, or both.

Tariff arguments get messy fast because they mix three ideas that are not the same:

  • which products are exposed
  • what happens to import costs after the policy
  • what consumers actually see on the shelf

People usually jump straight from headline to blame:

  • tariff announced
  • price rose
  • tariff did it

That can be directionally right and still analytically sloppy.

A tariff can be a real cause of higher shelf prices while the raw tariff-versus-no-tariff comparison overstates the effect because exposed products were never an ordinary comparison group.

This is exactly the kind of public economic question where a graph helps before regression ever enters the room.

Start with the ordinary retail argument

Suppose a retailer says the tariff is pushing up prices. A skeptic replies that exposed categories were already volatile, shipping costs were already moving, and those were not random items to begin with.

That is already a graph:

  • some categories are more import-dependent than others
  • import dependence affects both tariff exposure and baseline cost structure
  • shipping and import cost transmit part of the policy effect into the shelf price

Ask the graph question first

In a compact toy version:

  • Dependence: category import dependence
  • Tariff: the policy exposure level
  • Shipping: shipping-cost pressure
  • ImportCost: landed cost after the policy
  • ShelfPrice: consumer-facing price index

Drawn explicitly, that retail story looks like this:

Tariff and shelf price DAG

With py-bbn, the first thing you want to know is the backdoor story.

import networkx as nx

from pybbn.graphical import get_graph_tuple, get_minimal_confounders, get_paths

g = nx.DiGraph()
g.add_edges_from(
    [
        ("Dependence", "Shipping"),
        ("Dependence", "Tariff"),
        ("Dependence", "ShelfPrice"),
        ("Shipping", "ImportCost"),
        ("Tariff", "ImportCost"),
        ("ImportCost", "ShelfPrice"),
        ("Shipping", "ShelfPrice"),
        ("Tariff", "ShelfPrice"),
    ]
)

gt = get_graph_tuple(g)

get_minimal_confounders(gt, "Tariff", "ShelfPrice")
# ['Dependence']

get_paths(gt, "Tariff", "ShelfPrice")


That is a useful answer because it separates two different mistakes:

  • failing to account for category dependence
  • controlling away the very mediator path through ImportCost that you are trying to measure as part of the total effect

Use py-scm for the cost and price indices

import numpy as np

from pyscm.reasoning import create_reasoning_model


def build_linear_model(nodes, weighted_edges):
    idx = {node: i for i, node in enumerate(nodes)}
    B = np.zeros((len(nodes), len(nodes)))
    D = np.eye(len(nodes))

    for parent, child, weight in weighted_edges:
        B[idx[child], idx[parent]] = weight

    A = np.eye(len(nodes)) - B
    cov = np.linalg.inv(A) @ D @ np.linalg.inv(A).T

    return create_reasoning_model(
        {"nodes": nodes, "edges": [(p, c) for p, c, _ in weighted_edges]},
        {"v": nodes, "m": [0.0] * len(nodes), "S": cov.tolist()},
    )


nodes = ["Dependence", "Shipping", "Tariff", "ImportCost", "ShelfPrice"]
weighted_edges = [
    ("Dependence", "Shipping", 0.8),
    ("Dependence", "Tariff", 0.9),
    ("Dependence", "ShelfPrice", 0.6),
    ("Shipping", "ImportCost", 0.7),
    ("Tariff", "ImportCost", 1.3),
    ("ImportCost", "ShelfPrice", 0.9),
    ("Shipping", "ShelfPrice", 1.1),
    ("Tariff", "ShelfPrice", 0.2),
]

model = build_linear_model(nodes, weighted_edges)


The exposed categories look more expensive than the tariff effect alone

If you just compare tariff-exposed and non-exposed categories:

mean, _ = model.pquery({"Tariff": 1.0})
float(mean["ShelfPrice"])
# 2.3565


That is the kind of number that ends up in an argument thread. Tariff-exposed goods look about 2.36 units more expensive on the shelf-price index.

But that still bundles together tariff exposure and the fact that high-dependence categories were structurally different to begin with.

The intervention isolates the policy move

model.iquery("ShelfPrice", {"Tariff": 1.0})
# mean 1.37

model.iquery("ShelfPrice", {"Tariff": 0.0})
# mean 0.00

model.equery("ShelfPrice", {"Tariff": 1.0}, {"Tariff": 0.0})
# mean 1.37


In this toy model, the tariff still raises the shelf-price index. The point is not to make the tariff disappear. The point is to keep the estimate from borrowing extra lift from the category mix.

So the interventional effect is about +1.37, not the +2.36 implied by the raw exposed-versus-unexposed comparison.

Counterfactuals make the story product-level

Now imagine one specific item with a factual shelf-price index of 3.3 under the tariff regime.

model.cquery(
    "ShelfPrice",
    factual={
        "Dependence": 1.1,
        "Shipping": 0.8,
        "Tariff": 1.0,
        "ImportCost": 2.0,
        "ShelfPrice": 3.3,
    },
    counterfactual=[{"Tariff": 0.0}],
)


In this toy counterfactual, the same item falls to about 1.93 without the tariff.

That product-level counterfactual is easier to read as a concrete before-and-after for the same item:

Same item counterfactual without the tariff

It keeps the category and shipping backdrop fixed and changes only the tariff state.

That is a better retail question than “did the tariff matter?” It asks: for this class of exposed item, how much of the shelf move can plausibly be traced to the policy once you separate it from the category itself?

What this buys you

This is why graph queries matter alongside numeric inference:

  • the graph tells you what kind of comparison is broken
  • the intervention tells you the policy effect
  • the counterfactual turns the same model into a product-level story people can understand

That is a much better way to reason about trade-policy claims than arguing from the headline and a screenshot of prices.

Next in the series: falling vaccination can matter before an outbreak, but confirmed-case slices can still send you in the wrong direction.

Leave a Reply

Discover more from Rocket Vector Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading