{#- Brand mark: a beam enters an equilateral prism and the spectrum emerges staggered along the right face, each ray fanning out at its own angle. -#} {# The mark is base64-embedded rather than linked, so a report stays a single offline file. If the packaged asset is missing the masthead falls back to the wordmark alone rather than showing a broken image. #} {% if favicon_uri %} {% endif %}
{% set verdict = result.metadata.get('verdict') %} {% set consensus = result.evidence | selectattr('kind', 'equalto', 'anomaly_consensus_review') | list %} {# Both the anomaly review and the regression influence pass end in a list of rows a person should open, so both feed the same masthead counter. #} {% set regression_review = result.evidence | selectattr('kind', 'equalto', 'regression_review_rows') | list %} {% set review_count = consensus[0].value.review_row_count if consensus else (regression_review[0].value.row_count if regression_review else 0) %}{{ result.summary }}
{{ ai.narrative }}
{% endif %} {% if ai.column_reads %}{{ read.column }}{{ read.meaning }}{{ read.child }} → {{ read.parent }}{{ read.reading }}Interpretation by the AI investigator ({{ result.metadata.get('model', 'model') }}) over the deterministic evidence{% if ai.shared_labels %}, with column value samples shared per your privacy setting{% endif %} — judgment layered on the measurements above, not new measurements. Verify against the evidence.
What this run could not cover, and why.
{{ warning.code }}: {{ warning.message }}
{% endfor %} {% if result.sampling %} {% for record in result.sampling %}{{ record.operation }}: {{ record.sampled_rows | format_value }} of {{ record.source_rows | format_value }} rows, sampled with seed {{ record.seed }} ({{ record.strategy }}).{% for limitation in record.limitations %} {{ limitation }}{% endfor %}
{% endfor %} {% endif %}Minimal combinations only; business confirmation is required.
| Table | Columns | Unique | Complete | Confidence |
|---|---|---|---|---|
| {{ item.scope.table }} | {{ item.scope.columns | join(' + ') }} | {{ "%.1f%%" | format(item.value.uniqueness_rate * 100) }}{% if item.value.sampled %} sampled{% endif %} | {{ "%.1f%%" | format(item.value.completeness_rate * 100) }} | {{ "%.1f%%" | format(item.confidence * 100) }} |
Direction runs from the unique parent key to the referencing child columns. Sorted by confidence — filter by table to focus a pair.
| Parent | Child | Cardinality | Included | Orphan rows | Confidence |
|---|---|---|---|---|---|
{{ item.value.parent_table }}.{{ item.value.parent_columns | join(' + ') }} |
{{ item.value.child_table }}.{{ item.value.child_columns | join(' + ') }} |
{{ item.value.cardinality | replace('_', '–') }} | {{ "%.1f%%" | format(item.value.inclusion_rate * 100) }} | {{ item.value.orphan_row_count }} | {{ "%.0f%%" | format(item.confidence * 100) }}{% if item.value.sampled %} sampled{% endif %} |
{{ artifact.metadata.description }}
{% endif %}| {{ column.label }} | {% endfor %}
|---|
| {{ row[column.key] | format_value }} | {% endfor %}
Things wrong with the data: values that are missing, duplicated, mistyped, or standing in for something else. Ranked by severity.
{% else %}Ranked by severity so the most important issues are reviewed first.
{% endif %}{{ finding.summary }}
{% if finding.recommendation %}{{ finding.recommendation }}
{% endif %}| {{ column.label }} | {% endfor %}
|---|
| {{ row[column.key] | format_value }} | {% endfor %}
True of this data, and worth knowing — but not defects. Nothing here needs fixing; some of it changes how you model.
{{ finding.summary }}
{% if finding.recommendation %}{{ finding.recommendation }}
{% endif %}{{ item.value.rows | length }} row{% if item.value.rows | length != 1 %}s{% endif %} that move the fit more than any others — extreme in the features, badly predicted, or both. A data-entry error and a genuinely unusual record look identical here and need opposite treatment, so these are candidates, not corrections.
| Row | Actual | Predicted | Residual | Std. residual | Leverage | Cook's D | Why |
|---|---|---|---|---|---|---|---|
| {{ row.row_index }} | {{ row.actual | format_value }} | {{ row.predicted | format_value }} | {{ row.residual | format_value }} | {{ row.standardized_residual | format_value }} | {{ row.leverage | format_value }} | {{ row.cooks_distance | format_value }} | {{ row.reasons | join(', ') }} |
| {{ row.extreme_features | why_bars_svg }} | |||||||
Residual σ for this fit is {{ item.value.residual_std | format_value }}. Cook's distance is screened at the 4/n convention — a rule of thumb for “worth opening”, not a test of significance.
What the diagnostic probe left over. Everything here describes that fit on this data — it is model-conditional, not a property of the dataset.
A trustworthy fit straddles the zero rule evenly at every prediction. The band is the local spread: where it fans out, one prediction interval will not hold.{% if item.value.sampled %} Showing a deterministic sample of {{ item.value.point_count }} points.{% endif %}
{{ item.value | residual_scatter_svg }}Bars above the rule are ranges the model under-predicts; below, ranges it over-predicts. A fit can average out to zero error and still be wrong at both ends.
{{ item.value | conditional_bias_svg }}skew {{ item.value.skewness | format_value }} · kurtosis {{ item.value.kurtosis | format_value }}{% if item.value.normality_distance is not none %} · normality distance {{ item.value.normality_distance | format_value }}{% endif %}
{{ item.value.distribution | histogram_svg }}The normality figure is a Kolmogorov–Smirnov distance, not a p-value: the reference is estimated from these same residuals.
Skew is a property of {{ item.value.target }}, not a defect in it. Shown so you can decide, not so you feel obliged to transform.
{{ item.value.shape | replace('_', ' ') }} · skew {{ item.value.skewness | format_value }}
{{ item.value.distribution_raw | histogram_svg }}skew {{ item.value.best_skewness_after | format_value }} — measured on this data, not assumed
{{ item.value.distribution_transformed | histogram_svg }}| Transform | Applicable | Skew after | Reduction | Note |
|---|---|---|---|---|
| {{ candidate.transform }} | {{ candidate.applicable | format_value }} | {{ candidate.skewness_after | format_value }} | {{ candidate.skew_reduction | format_value }} | {{ candidate.reason }} |
A transformed target changes what an error means; back-transformed predictions are biased unless corrected.
{{ item.value.segment_count }} candidate groups. Each feature is described as a distance from the overall average in standard deviations, so features measured in different units are directly comparable. These are candidates: clustering has no ground truth to be right about.
{% for feature in segment.distinguishing_features %}{{ feature.feature }} {{ feature.direction }} ({{ '%+.2f' % feature.z }}σ, {{ feature.segment_mean | format_value }} vs {{ feature.overall_mean | format_value }}){% endfor %}
{% else %}Nothing distinguishes this group: every feature sits within half a standard deviation of the overall average.
{% endif %} {# Only over-represented categories earn a chip. A category sitting at its own baseline share says nothing, and repeating it on every card would bury the ones that do. #} {% set notable = segment.categories | selectattr('over_represented') | list %} {% if notable %}{% for cat in notable %}{{ cat.column }}: {{ cat.top_value }} {{ '%.0f' % (cat.share * 100) }}%{% if cat.lift %} ({{ '%.1f' % cat.lift }}x overall){% endif %}{% endfor %}
{% endif %} {% if segment.representatives %}| Row | {% for name in item.value.segments[0].representatives[0]['values'].keys() %}{{ name }} | {% endfor %}
|---|---|
| {{ row.row_index }} | {% for value in row['values'].values() %}{{ value | format_value }} | {% endfor %}
The real records nearest this group's centre, not an average that may resemble nobody.
A two-dimensional projection carrying {{ '%.0f' % (embedding.value.captured_variance * 100) }}% of the variance. One panel per group rather than one colour per group, so nothing depends on telling similar hues apart. A projection is a visual aid, never evidence that groups exist.
Silhouette measures how tidy a partition is — noise cut into k pieces scores on it too. Stability is the adjusted Rand index between two independently clustered subsamples, and it is the only line here that can fail.
No k cleared both bars — separation of at least {{ item.value.weak_silhouette_below }} and stability of at least {{ item.value.unstable_below }}. That is a result, not a gap in the analysis.
{% endif %}Asked before any clustering was fitted, because afterwards the output looks equally convincing either way.
| Check | Value | Reading |
|---|---|---|
| Cluster tendency (Hopkins) | {{ item.value.hopkins_mean | format_value }} ({{ item.value.hopkins_min | format_value }}–{{ item.value.hopkins_max | format_value }} over {{ item.value.repeats }} samples) | {% if item.value.verdict == 'no_tendency' %}About 0.5 — indistinguishable from uniform noise{% elif item.value.verdict == 'clustered' %}Well above 0.5 — points bunch together{% else %}Slightly above 0.5 — weak tendency{% endif %} |
| Distance contrast | {{ item.value.relative_contrast | format_value }} | {% if item.value.distances_concentrated %}Concentrated — every point sits about as far from every other{% else %}Distances still discriminate between near and far{% endif %} |
| Effective dimensions | {{ item.value.intrinsic_dimensionality | format_value }} of {{ item.value.feature_count }} | Principal components holding 90% of the variance |
| Agreement without standardizing | {{ item.value.scaling_agreement | format_value }} | {% if item.value.scaling_changes_answer %}Scaling changes which rows group together{% else %}The grouping survives the scaling choice{% endif %} |
Agreement with the reference partition after removing each feature. A grouping that survives every removal is a summary of the whole feature set; one that does not is a restatement of the feature that broke it.
| Removed feature | Agreement (ARI) | |
|---|---|---|
| {{ drop.feature }} | {{ drop.agreement_without_it | format_value }} | {% if drop.dominant %}The grouping is largely this feature{% endif %} |
{{ artifact.metadata.description }}
{% endif %}| {{ column.label }} | {% endfor %}
|---|
| {{ row[column.key] | format_value }} | {% endfor %}
{{ item.value.value }} over time, at {{ item.value.frequency_label }} spacing. The line breaks where nothing was recorded rather than drawing across the gap, and dashed rules mark detected level shifts.
What the series is made of. Neither is a defect — a strong {{ item.value.seasonal_label }} cycle is usually the most useful thing in a series.{% if item.value.computed_on_interpolated_series %} Computed on the regularized reconstruction, not the raw rows.{% endif %}
trend strength {{ item.value.trend_strength | format_value }} · {{ item.value.trend_direction }} by {{ item.value.trend_change | format_value }} across the history
{{ {'points': item.value.observed_points, 'trend_points': item.value.trend_points, 'value': item.value.value} | series_line_svg }}seasonal strength {{ item.value.seasonal_strength | format_value }} · peak-to-trough {{ item.value.seasonal_peak_to_trough | format_value }}
{{ item.value | seasonal_profile_svg }}How far back the series predicts itself, and whether its level holds still. Both describe the series; neither is something to fix.
Bars inside the shaded band are indistinguishable from no correlation.{% if item.value.seasonal_candidates %} Peaks at {{ item.value.seasonal_candidates | map(attribute='period') | join(', ') }} suggest a repeating cycle.{% endif %}
{{ item.value | acf_stems_svg }}| Test | Null hypothesis | Statistic | p-value | Rejects null |
|---|---|---|---|---|
| ADF | {{ item.value.adf.null_hypothesis }} | {{ item.value.adf.statistic | format_value }} | {{ item.value.adf.p_value | format_value }} | {{ item.value.adf.rejects_null | format_value }} |
| KPSS | {{ item.value.kpss.null_hypothesis }} | {{ item.value.kpss.statistic | format_value }} | {{ item.value.kpss.p_value | format_value }} | {{ item.value.kpss.rejects_null | format_value }} |
{{ item.value.explanation }}{% if not item.value.tests_agree %} The two tests disagree, and that disagreement is the informative result — it distinguishes a deterministic trend from a shifting level.{% endif %}
{% endfor %}Absence comes in two forms and they have different causes. A period with no row at all is a collection failure; a period with a row and a blank value is a measurement failure. Counting them together would hide both.
| Kind | From | To | {{ item.value.frequency_label | capitalize }} periods |
|---|---|---|---|
| No row recorded | {{ block.start[:10] }} | {{ block.end[:10] }} | {{ block.periods }} |
| Row present, value blank | {{ block.start[:10] }} | {{ block.end[:10] }} | {{ block.periods }} |
{{ item.value.unrecorded_period_count }} of {{ item.value.expected_periods }} period(s) unrecorded, {{ item.value.blank_period_count }} blank.
{{ artifact.metadata.description }}
{% endif %}| {{ column.label }} | {% endfor %}
|---|
| {{ row[column.key] | format_value }} | {% endfor %}
{{ artifact.metadata.description }}
{% endif %}| {{ column.label }} | {% endfor %}
|---|
| {{ row[column.key] | format_value }} | {% endfor %}
{{ item.value.review_row_count }} of {{ item.value.evaluated_row_count }} row{% if item.value.evaluated_row_count != 1 %}s{% endif %}, ranked by how many independent checks flagged them. Highlighted cells are the values that stand out; expand a row to see how each check flagged it.
| Agreement | {% for column in item.value.columns %}{{ column }} | {% endfor %}Why it stands out |
|---|---|---|
| {{ row.method_count }}/{{ item.value.total_detectors }} {{ row.methods | join(' · ') }} | {% for column in item.value.columns %}{{ row['values'][column] | format_cell }} | {% endfor %}
{{ row.why }}
{% if ex.get('univariate') %}{{ ex.univariate.contributors | why_bars_svg }}{% endif %}
{% if mv or cond %}
How each check flagged this{% if mv %}Multivariate — joint deviation score {{ mv.score | round(1) }} (flags at ≥ {{ mv.threshold | round(1) }}). Every column's distance from its own typical value:
{{ mv['columns'] | why_bars_svg }}
Conditional — {{ cond.value_column }} is unusual for the {{ cond.condition_column }} ≈ {{ cond.condition_value | format_cell }} peer group. Where this row sits vs its peers:
{{ cond | peer_group_svg }}
|
Every image the checks above called out, shown rather than listed. Duplicate candidates are paired side by side so you can confirm or dismiss them without opening a file browser.
One dot per distinct image size — a single dot means the dataset already agrees on a shape, a spread means your resize policy has to make a decision.
{{ dimension.value.unique_dimensions | format_value }} distinct size{% if dimension.value.unique_dimensions != 1 %}s{% endif %} · dot size = image count
{{ dimension.value | image_dimension_svg }}median {{ brightness.value.box.median | format_cell }} · 0 is black, 1 is white
{{ brightness.value | histogram_svg }}{{ artifact.metadata.description }}
{% endif %}| {{ column.label }} | {% endfor %}
|---|
| {{ row[column.key] | format_value }} | {% endfor %}
Where each numeric column's values sit. Bars are row counts; the strip below each shows the median, the middle 50%, and the whiskers. Red marks the flagged values.
Two populations — {{ item.value.modality.clusters[0].count }} row(s) up to {{ item.value.modality.clusters[0].max | format_cell }}, then {{ item.value.modality.clusters[1].count }} row(s) from {{ item.value.modality.clusters[1].min | format_cell }} up.
{% else %}median {{ item.value.box.median | format_cell }} · middle 50% {{ item.value.box.q1 | format_cell }}–{{ item.value.box.q3 | format_cell }}
{% endif %} {{ item.value | histogram_svg }}Each point is a row; red points are the flagged rows. Shows whether a flagged row is also unusual in context, not just on one column.
{{ artifact.metadata.description }}
{% endif %}| {{ column.label }} | {% endfor %}
|---|
| {{ row[column.key] | format_value }} | {% endfor %}
{{ table.row_count | format_value }} image files · {{ table.column_count | format_value }} manifest fields
{% else %}{{ table.row_count | format_value }} rows · {{ table.column_count | format_value }} columns · {{ table.duplicate_row_count | format_value }} duplicate row{% if table.duplicate_row_count != 1 %}s{% endif %}
{% endif %}{{ shape.descriptors | join('; ') }}
{% endif %} {% elif frequency %} {{ frequency | category_bars_svg }} {% elif timeline %} {{ timeline | timeline_svg }} {% elif column.top_values and column.row_count %}{% if item.value is none %}(missing){% else %}{{ item.value | string | truncate(24, true, '…') }}{% endif %} {{ "%.0f%%" | format(item.count / column.row_count * 100) }}{% endfor %}
No chart for this column.
{% endif %}{% if fit.family %} Closest standard distribution: {{ fit.label }}{% for name, value in fit.parameters.items() %}{% if loop.first %} ({% endif %}{{ name }}={{ value | format_value }}{% if loop.last %}){% else %}, {% endif %}{% endfor %} — {{ fit.quality }} fit, KS distance {{ "%.3f" | format(fit.ks_distance) }}. {% elif fit.reason == 'no_family_fits_well' %} No standard distribution fits this column well (closest is {{ fit.ranked[0].label }} at KS distance {{ "%.3f" | format(fit.best_distance) }}). {% elif fit.reason == 'too_few_values' %} Too few distinct values to fit a distribution. {% endif %} The KS distance is measured with parameters estimated from this same column, so it describes how close the fit is — it is not a hypothesis test, and no p-value is implied.
{% endif %} {% if frequency and frequency.other_category_count %}{{ frequency.other_category_count | format_value }} further label(s) are folded into “other”.
{% endif %}How strongly each pair of columns moves together, on a 0–1 scale. Three statistics share that scale because the columns are of different types: Spearman ρ between two numbers (signed, so the cell shows direction), Cramér's V between two categories, and the correlation ratio between a category and a number. Hover a cell for the exact figure and the statistic used. Association is not causation.
| Pair | Strength | Statistic | Reading |
|---|---|---|---|
| {{ pair.left }} & {{ pair.right }} | {% if pair.method == 'spearman' %}{{ "%+.2f" | format(pair.spearman) }}{% else %}{{ "%.2f" | format(pair.strength) }}{% endif %} | {% if pair.method == 'spearman' %}Spearman ρ{% if pair.pearson is not none %} (Pearson {{ "%+.2f" | format(pair.pearson) }}){% endif %}{% elif pair.method == 'cramers_v' %}Cramér's V{% else %}correlation ratio{% endif %} | {% if pair.strength >= 0.99 %}Effectively one variable.{% elif pair.method == 'spearman' %}Move {% if pair.spearman > 0 %}together{% else %}in opposition{% endif %}.{% else %}One largely determines the other.{% endif %} |
No pair reaches 0.80, so no column here is a restatement of another.
{% endif %}Each point is a row. The pairs below are the most strongly associated ones; use the selector to plot any other pair. A correlation number says how much two columns move together — a scatter says how, which is where curves, clusters, and ceilings show up.
association {{ "%.2f" | format(item.value.association) }}{% if item.value.point_count < item.value.available_point_count %} · {{ item.value.point_count | format_value }} of {{ item.value.available_point_count | format_value }} rows drawn{% endif %}
{{ item.value | scatter_svg(true) }}association {{ "%.2f" | format(item.value.association) }} · {{ item.value.point_count | format_value }} rows drawn
{{ item.value | scatter_svg(true) }}{{ missingness.complete_row_count | format_value }} of {{ missingness.row_count | format_value }} rows are complete. Bars past 20% are the ones the profile promotes to an issue.
How often two columns are blank on the same row. A high overlap usually means one optional section of a form, or a field added partway through collection — which is a different problem from values lost at random.
The first and last rows as they were loaded{% if sample.hidden_column_count %}, showing {{ sample.columns | length }} of {{ sample.columns | length + sample.hidden_column_count }} columns{% endif %}. Every summary above is a claim about these values; this is what they actually look like.
| # | {% for name in sample.columns %}{{ name }} | {% endfor %}
|---|---|
| {{ row.index }} | {% for value in row['values'] %}{{ value | format_cell }} | {% endfor %}
| ⋯ | {% for name in sample.columns %}⋯ | {% endfor %}
| {{ row.index }} | {% for value in row['values'] %}{{ value | format_cell }} | {% endfor %}
{{ duplicates[0].value.duplicate_row_count | format_value }} row(s) repeat an earlier row exactly. Examples:
| Copies | Row indexes | {% for name in duplicates[0].value.columns %}{{ name }} | {% endfor %}
|---|---|---|
| {{ group.row_count }} | {{ group.indexes | join(', ') }} | {% for value in group['values'] %}{{ value | format_cell }} | {% endfor %}
These are recommendations only. Prism EDA has not modified the data.
{{ step.table }}{% if step.columns %}.{{ step.columns | join(', ') }}{% endif %}: {{ step.rationale }}