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

# Cohort analysis using a thin spine

> Build cohort analyses using thin dimensional spines.

export const categoryIcons = {
  'administration': 'lock',
  'api': 'terminal',
  'connections': 'database',
  'dashboards': 'table-columns',
  'embed': 'code',
  'errors': 'exclamation',
  'modeling': 'wrench',
  'patterns': 'plus',
  'schedules & alerts': 'envelope',
  'visualizations': 'chart-column',
  'workbooks': 'book'
};

export const GuideSidebar = ({category, relatedLinks, updatedDate}) => {
  const [progress, setProgress] = React.useState(0);
  React.useEffect(() => {
    const sidebar = document.querySelector('.guide-sidebar');
    if (!sidebar) return;
    let container = sidebar.parentElement;
    while (container && !container.querySelector('.guide-header')) {
      container = container.parentElement;
    }
    if (container && !container.classList.contains('guide-page-layout')) {
      container.classList.add('guide-page-layout');
    }
  }, []);
  React.useEffect(() => {
    const handleScroll = () => {
      const scrollTop = window.scrollY;
      const docHeight = document.documentElement.scrollHeight - window.innerHeight;
      const scrollPercent = docHeight > 0 ? scrollTop / docHeight * 100 : 0;
      setProgress(Math.min(100, Math.max(0, scrollPercent)));
    };
    window.addEventListener('scroll', handleScroll, {
      passive: true
    });
    handleScroll();
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);
  const icon = category ? categoryIcons[category.toLowerCase()] || 'book' : 'book';
  return <aside className="guide-sidebar">
      <div className="guide-sidebar-content">
        <a href="/guides" className="guide-sidebar-back">
          <Icon icon="arrow-left" iconType="solid" size={14} />
          <span>All guides</span>
        </a>

        <div className="guide-sidebar-section">
          <div className="guide-sidebar-label">Progress</div>
          <div className="guide-sidebar-progress">
            <div className="guide-mascot">
              <svg viewBox="0 0 450 450" width="48" height="48">
                <defs>
                  <clipPath id="progressClip">
                    <rect x="0" y={450 - progress * 4.5} width="450" height={progress * 4.5} />
                  </clipPath>
                  <linearGradient id="blobbyGradient" x1="55.9753" y1="0" x2="492.197" y2="169.724" gradientUnits="userSpaceOnUse">
                    <stop stopColor="#BCA2F3" />
                    <stop offset="0.572917" stopColor="#FF7AA2" />
                    <stop offset="1" stopColor="#F3D4A2" />
                  </linearGradient>
                </defs>

                {}
                <circle cx="223.901" cy="223.901" r="213.901" transform="matrix(-0.999988 -0.0049013 0.00491945 -0.999988 447.797 449.992)" fill="#FAFAFA" stroke="#480B38" strokeWidth="20" />

                {}
                <circle cx="223.901" cy="223.901" r="213.901" transform="matrix(-0.999988 -0.0049013 0.00491945 -0.999988 447.797 449.992)" fill="url(#blobbyGradient)" stroke="#480B38" strokeWidth="20" clipPath="url(#progressClip)" />

                {}
                <path d="M310.41 195.084C310.41 200.052 301.362 212.472 284.328 212.472C266.585 212.472 258.246 201.294 258.246 195.912" stroke="#480B38" strokeWidth="17.3883" strokeMiterlimit="1.33344" strokeLinecap="round" />
                <circle cx="21.168" cy="21.168" r="21.168" transform="matrix(-1 0 0 1 388.658 169.001)" fill="#480B38" />
                <circle cx="21.168" cy="21.168" r="21.168" transform="matrix(-1 0 0 1 223.467 169.001)" fill="#480B38" />
              </svg>
            </div>
            <span className="guide-sidebar-progress-text">{Math.round(progress)}%</span>
          </div>
        </div>

        {category && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Category</div>
            <div className="guide-sidebar-category">
              <Icon icon={icon} iconType="solid" size={14} />
              <span>{category}</span>
            </div>
          </div>}

        {updatedDate && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Last updated</div>
            <div className="guide-sidebar-date">{updatedDate}</div>
          </div>}

        {relatedLinks && relatedLinks.length > 0 && <div className="guide-sidebar-section">
            <div className="guide-sidebar-label">Related</div>
            <ul className="guide-sidebar-links">
              {relatedLinks.map((link, index) => <li key={index}>
                  <a href={link.href}>{link.title}</a>
                </li>)}
            </ul>
          </div>}
      </div>
    </aside>;
};

export const GuideTitle = ({title}) => {
  return <div className="guide-header">
      <h1 className="guide-title">{title}</h1>
    </div>;
};

<GuideSidebar
  categoryIcons={categoryIcons}
  category="patterns"
  updatedDate="February 2026"
  relatedLinks={[
{ title: "Thin dimension spine pattern", href: "/guides/patterns/thin-dimension-spine" },
{ title: "Templated filters", href: "/modeling/templated-filters" },
{ title: "Measures", href: "/modeling/measures" },
{ title: "Topics", href: "/modeling/topics" }
]}
/>

<GuideTitle title="Cohort analysis using a thin spine" />

In the [Thin dimensional spine for multi-fact event analysis guide](/guides/patterns/thin-dimension-spine), you learned what a thin dimensional spine is, how it works, and how to implement your own using dbt and Omni.

In this guide, we'll build on the knowledge in that guide to walk you through several approaches to cohort analysis.

## Requirements

To follow along with this guide, you'll need:

* **To have read the [Thin dimensional spine for multi-fact event analysis guide](/guides/patterns/thin-dimension-spine).** This guide describes the basics of thin dimensional spines and how to build and materialize your own.
* **Familiarity with [modeling](/modeling) in Omni**

## Attribute-based cohorts

To build this type of analysis:

1. Maintain an attribute table with per-user first/last timestamps, e.g., `attr__users_event_bounds` with `first_signup_ts`, `first_purchase_ts`.
2. Group and filter users by these milestones across event streams

## Event-to-event cohorts

This type of analysis looks at the timing between events — for example, signup-to-first-login latency, or engagement patterns following a first purchase.

## Dynamic 'time since selected event' cohorting

Using the spine, you can model a dynamic "since basis" and "grain" selector to compute buckets of time since a specific event:

```yaml title="Snippet: dim__user_event_spine_thin.view" expandable theme={null}
dimensions:
  selected_basis_ts:
    type: timestamp
    sql: |
      case {{filters.dim__user_event_spine_thin.cohort_basis.value}}
        when 'first_ad' then ${attr__users_event_bounds.first_ad_ts}
        when 'first_signup' then ${attr__users_event_bounds.first_signup_ts}
        else ${attr__users_event_bounds.first_ad_ts}
      end

  time_since_selected_event:
    type: number
    label: Time Since Selected Event
    sql: |
      datediff(
        case {{filters.dim__user_event_spine_thin.cohort_grain.value}}
          when 'day' then 'day'
          else 'day'
        end,
        ${selected_basis_date},
        ${event_date_day}
      )
```
