3. DAX fundamentals for injury metrics
Injury metrics from CAE output are rarely a single number. A typical crashworthiness study produces, for each simulation run, a time history of head acceleration, chest deflection, femur force, and belt force, from which scalar injury criteria such as HIC15, HIC36, chest deflection peak, and Nij are derived. When these results are loaded into Power BI, the analyst must decide, for every derived quantity, whether it should be materialized as a calculated column or evaluated on demand as a measure. That decision is not stylistic. It determines the storage cost of the model, the correctness of aggregations across runs and configurations, and whether slicers and cross-filters behave as expected. This chapter establishes the two evaluation contexts that underpin every DAX expression — row context and filter context — and shows how they map onto the CAE result table.
The shape of a CAE injury results table
Consider a simplified fact table, InjuryResults, with one row per occupant per simulation run:
InjuryResults
RunID | Configuration | Occupant | BodyRegion | Metric | Value | Unit | Limit
R-001 | Baseline | Driver | Head | HIC15 | 612.4 | - | 700
R-001 | Baseline | Driver | Chest | Defl_max | 34.2 | mm | 42
R-001 | Baseline | Passenger| Head | HIC15 | 588.1 | - | 700
R-002 | Alt_A | Driver | Head | HIC15 | 731.8 | - | 700
R-002 | Alt_A | Driver | Chest | Defl_max | 38.9 | mm | 42
Each row is already a scalar summary of a time history; the time-series data lives elsewhere (or is pre-processed upstream). The analytical questions are: what is the worst HIC15 across all runs, how many runs exceed the limit, and what is the margin per configuration? Each question implies a different evaluation context.
Row context: computing per-row derived values
A row context exists when DAX evaluates an expression for a single row of a table. Calculated columns always evaluate in row context: the engine iterates the table, and for each row the column references resolve to that row's values. If we want a per-row margin, we add a calculated column:
Margin = InjuryResults[Limit] - InjuryResults[Value]
LimitExceeded = IF ( InjuryResults[Value] > InjuryResults[Limit], 1, 0 )
Both columns are computed once at refresh, stored in the VertiPaq model, and compressed alongside the rest of the table. For a table with a few thousand rows this is negligible; for a table with millions of time-step rows it is not. The critical property of a calculated column is that it is row-scoped: it cannot see the whole table, only the current row, unless you explicitly lift the row context with an iterator such as SUMX or MAXX.
This is where analysts new to DAX most often go wrong. Writing SUM ( InjuryResults[Value] ) inside a calculated column does not produce a per-row value; it produces the grand total of the entire column, repeated on every row, because the aggregation ignores the row context. To compute a per-run total inside a calculated column you must use an iterator:
RunTotalValue =
SUMX (
FILTER ( InjuryResults, InjuryResults[RunID] = EARLIER ( InjuryResults[RunID] ) ),
InjuryResults[Value]
)
EARLIER captures the outer row's RunID so the inner FILTER can restrict the table to the same run. This works, but it is expensive and, more importantly, it is usually the wrong tool: totals across runs are exactly what measures are for.
Filter context: aggregating across the model
A filter context is the set of filters applied to the model at the moment a measure is evaluated — slicers, visual axes, page filters, and cross-filter selections from other visuals. Measures have no row context of their own; they are evaluated once per cell of a visual, inside whatever filter context that cell defines. A measure is never stored; it is computed at query time.
Worst HIC15 =
CALCULATE (
MAX ( InjuryResults[Value] ),
InjuryResults[Metric] = "HIC15"
)
Runs Exceeding Limit =
COUNTROWS (
FILTER (
InjuryResults,
InjuryResults[Value] > InjuryResults[Limit]
)
)
Margin to Limit =
MIN ( InjuryResults[Limit] ) - MAX ( InjuryResults[Value] )
Placed in a matrix with Configuration on rows and Occupant on columns, each cell evaluates Worst HIC15 under the filter context defined by that configuration–occupant intersection. Change the slicer from Baseline to Alt_A and every cell recomputes. No column is stored, and the measure automatically respects any future slicing dimension — test facility, sled configuration, dummy type — without modification.
How the two contexts interact
The interaction becomes concrete when a measure needs a row-level value. CALCULATE transitions row context into filter context: inside an iterator, CALCULATE can take the current row's values and turn them into filters on the model. This is the mechanism behind context transition, and it is why SUMX ( InjuryResults, [SomeMeasure] ) behaves differently from SUMX ( InjuryResults, InjuryResults[Value] ) — the first evaluates the measure once per row with that row's values as filters, the second simply sums a column.
For injury metrics, a common and correct pattern is to keep the raw scalar results in the fact table, add only genuinely row-level attributes as calculated columns (for example, a Status column classifying each result as Pass, Marginal, or Fail against its limit), and express every cross-run statistic as a measure. The SVG below maps this division of labour onto the actual objects in the model.
Applying the distinction to a concrete question
Suppose the safety team asks: "For each configuration, how many runs exceed the HIC15 limit, and what is the worst margin?" The correct implementation keeps InjuryResults as a narrow fact table and defines two measures. Runs Exceeding Limit uses FILTER to test each row against its own limit, then COUNTROWS to count survivors; because it is a measure, the count is automatically scoped to whatever configuration the visual displays. Margin to Limit uses MIN(Limit) − MAX(Value) so that the reported margin is the worst case within the current filter context, not an average that could mask a failing run.
If instead the analyst materializes LimitExceeded as a calculated column and then writes SUM(InjuryResults[LimitExceeded]) as a measure, the result is numerically identical for simple cases but the model now carries an extra column, and any change to the limit definition requires a full refresh rather than a measure edit. The calculated column is justified only when the flag must appear on a slicer or be used to sort or group rows — for example, filtering a table visual to show only failing results.
Metric and RunID. Keep the time history in a separate table related on RunID, and compute HIC or peak values upstream or in a dedicated measure that respects the relationship.Verification and common failure symptoms
After building the measures, verify them against a known subset. Filter the report to Configuration = "Alt_A" and Occupant = "Driver"; the matrix should show Worst HIC15 = 731.8 and Runs Exceeding Limit = 1 for the sample data above. If the measure returns the grand total across all configurations instead, the filter is being ignored — usually because the measure references a column from an unrelated table or because ALL has been applied inadvertently. If a calculated column returns the same value on every row, the expression contains a bare aggregation where a row-level reference was intended. If totals look correct but subtotals do not, check whether the measure uses SUM over a column that already contains a total, which double-counts.
The discipline is straightforward: use calculated columns for stable, row-level attributes that must be sliced or grouped; use measures for every statistic that must respond to filter context. In injury-metric reporting, almost every number the safety team cares about — worst HIC, count of failures, margin to limit, pass rate by configuration — is a measure. The calculated columns are the small set of descriptive flags and categories that make those measures easy to slice.