Healthcare Clinic Network Data Model

Healthcare Clinic Network Data Model

10 data marts150 fieldsVlad FlaksRus Obolonsky

Outpatient clinics across the US and Canada tracked from advertising spend through contact-centre enquiries and booked appointments to the revenue clinicians bill, with patient history and value carried across two currencies.

Overview

A network of multi-specialty outpatient clinics across the United States and Canada, modelled from the first advertising impression through to the revenue a clinician bills at the chair. Budget bought by keyword and creative brings visits to the website; a fraction of those visits ask for an appointment, most of them by picking up the phone; a small contact-centre team works those enquiries until they are booked or given up on; and the appointments that follow are either attended, cancelled in advance or simply not turned up to. Patients accumulate a visit history, a lifetime value and a recency segment on top of that, along with the treatment that was planned for them and never delivered. Locations bill in their own currency and every revenue figure is also carried converted, so a network spanning two countries reads as one business.

Scope: this model covers demand generation, intake and delivered care — what it costs to acquire a patient, whether the contact centre reaches them, whether they attend, and what the visit bills. The payer side is out of scope: insurance appears only as how a visit was settled, and there are no insurance plans, claims, reimbursements or denial rates here, so payer mix and revenue-cycle questions have no answer in this model. Nor does it model the schedule as capacity — there are appointments, but no slots or clinician availability — or a coded procedure catalogue. What it answers well is the whole path from spend to attended, billed care.

Example Questions

  • Which channels buy patients worth keeping rather than merely cheap ones — do the campaigns with the lowest cost per enquiry produce patients who return and accumulate real lifetime value, or ones who attend once and lapse?
  • Does the contact centre change the outcome — do enquiries someone actually reached get booked and attended more often than the ones that went unanswered, and how many attempts is that worth?
  • Where is capacity being lost, and what is it worth — how much revenue sits in no-shows and cancellations by location and specialty, and how much more sits in treatment that was planned and never delivered?

Explore on canvas →

Ad Spend Data Mart

Ad Spend

What the network paid to be found, at the finest grain it is billed: one row per day, channel, campaign, keyword, creative and country, with cost in the original currency and converted to a single one, alongside impressions and clicks. This is the mart to use when the question is which keyword or creative is consuming budget, rather than which channel — that coarser view is already rolled up with its outcomes in Attribution. Because healthcare acquisition costs vary enormously by channel, keyword-level spend is usually where a blended cost per patient turns out to be hiding one very expensive route and one very cheap one.

Fields

ColumnTypeAliasDescription
dateDATEDatePK. Calendar date the spend was incurred. FK to Attribution
sourceSTRINGSourcePK. Platform or network the spend went to, such as Google or Meta. FK to Attribution
mediumSTRINGMediumPK. Advertising model used, such as cpc. FK to Attribution
campaignSTRINGCampaign NamePK. Campaign the spend belongs to. FK to Attribution
keywordSTRINGKeywordPK. Search term the advertising bid on.
ad_contentSTRINGAd ContentPK. Creative or ad variant the spend ran against.
countrySTRINGCountryPK. Country the advertising was targeted at.
costFLOATCostSpend in the original billing currency.
cost_normalizedFLOATNormalized CostSpend converted to USD. Use this whenever markets are compared.
impressionsINTEGERImpressionsTimes the advertising was displayed.
clicksINTEGERClicksTimes the advertising was clicked.

Relationships

Related data martOnCardinalityMeaning
Attributiondate = date, source = source, medium = medium, campaign = campaignN:NThe acquisition funnel this day of spend paid for.

Attribution Data Mart

Attribution

The funnel on one row: for every day, channel, campaign and country, what was spent and what came back — sessions, enquiries, new patients, completed visits, revenue and the lifetime value now attached to the patients that channel produced. This is the mart that answers what a patient costs to acquire, and it is deliberately built on completed visits rather than on bookings, because a booking that turns into a no-show has not acquired anyone. Having spend and outcome side by side at the same grain is what makes cost per new patient comparable across channels without stitching marts together first.

Fields

ColumnTypeAliasDescription
attribution_idSTRINGAttribution IDPK. Unique identifier for this day, channel, campaign and country combination.
dateDATEDateCalendar date the spend and outcomes are attributed to.
sourceSTRINGTraffic SourceOrigin of the traffic, such as a search engine or social network.
mediumSTRINGTraffic MediumTraffic type, such as cpc, organic or referral.
campaignSTRINGCampaign NameMarketing campaign the spend and outcomes belong to.
countrySTRINGCountryCountry the activity is attributed to.
costFLOATRaw CostAdvertising spend in the original billing currency.
cost_normalizedFLOATNormalized CostAdvertising spend converted to USD. Use this for any cross-country comparison.
impressionsINTEGERImpressionsTimes the advertising was displayed.
clicksINTEGERClicksTimes the advertising was clicked.
sessionsINTEGERSessionsWebsite sessions attributed to this row.
leadsINTEGERLeadsEnquiries attributed to this row.
new_patientsINTEGERNew PatientsFirst-time patients attributed to this row. The usual denominator for acquisition cost.
completed_visitsINTEGERCompleted VisitsVisits that were actually attended. The stricter acquisition denominator, since a booking lost to a no-show acquires nobody.
revenue_normalizedFLOATEarly Revenue (USD)Revenue the attributed patients billed within 90 days of their first attended visit, in USD — how fast the acquisition cost on this row started coming back. Deliberately narrower than ltv: measured over the full relationship the two would be the same number.
ltvFLOATLifetime ValueTotal revenue the patients on this row have billed to date, in USD. Read against revenue_normalized to see how far eventual value runs ahead of the first 90 days.

Clinic Data Mart

Clinic

The network's footprint: one row per clinic location, with where it is, what currency it bills in and which practice-management system its records come from. Locations span the United States and Canada, so a clinic carries both its own billing currency and the state or province it sits in — the cut every regional comparison starts from. is_active separates locations that are open and taking patients from ones that have closed or not yet opened, which matters whenever a per-location average would otherwise be dragged down by a site that was dark for part of the period. This is the dimension that turns any patient, visit or enquiry number into a location-level one.

Fields

ColumnTypeAliasDescription
clinic_idSTRINGClinic IDPK. Unique identifier for this clinic location.
clinic_nameSTRINGClinic NamePublic-facing name of the location, as patients see it.
countrySTRINGCountryTwo-letter country code the location operates in: US or CA.
state_provinceSTRINGState / ProvinceState (US) or province (Canada) the location sits in, e.g. TX, CO, ON. The standard regional cut for comparing locations.
citySTRINGCityCity the location operates in.
addressSTRINGStreet AddressStreet address of the location.
phoneSTRINGPhone NumberPrimary contact number patients call to reach this location.
currencySTRINGLocal CurrencyCurrency this location bills patients in: USD or CAD. Revenue is also carried normalised to USD wherever it appears.
ehr_systemSTRINGEHR SystemPractice-management / electronic health record system the location's records originate from. Check this first when one clinic's data looks structurally unlike another's.
is_activeBOOLEANIs ActiveTrue while the location is open and accepting patients. Exclude inactive locations before comparing per-location averages.

Communications Data Mart

Communications

Every attempt to reach a patient, and every time one reached us: one row per interaction, with the channel it used, which direction it went, whether it succeeded, and which agent handled it. Calls dominate, because that is how most people still choose a clinician, and the honest measure of a contact centre lives here rather than in the enquiry record — how many attempts it takes before someone answers, which enquiries are given up on, and whether persistence actually changes the outcome. is_first and is_last bracket each conversation, and is_scheduling separates the interactions that were about getting an appointment in the diary from reminders, follow-ups and automated replies.

Fields

ColumnTypeAliasDescription
communication_idSTRINGCommunication IDPK. Unique identifier for this interaction.
lead_idSTRINGLead IDEnquiry this interaction concerns. FK to Leads
patient_idSTRINGPatient IDPatient this interaction concerns, once the enquiry has converted. FK to Patients
agent_idSTRINGAgent IDAgent who handled the interaction. FK to Patient Access Agent
typeSTRINGCommunication TypeChannel used: call, sms, email, web_chat, portal_message.
directionSTRINGDirectioninbound when the patient contacted us, outbound when we contacted them.
statusSTRINGStatusOutcome of the attempt: successful or unsuccessful. Counting unsuccessful outbound attempts is how the cost of chasing is measured.
subjectSTRINGSubjectShort label describing what the interaction was about.
message_textSTRINGMessage TextBody of the message sent or received.
is_autoreplyBOOLEANIs Auto ReplyTrue when the interaction was generated automatically rather than sent by a person.
creation_dateDATECreation DateDate the interaction took place.
is_firstBOOLEANIs First CommunicationTrue for the first interaction recorded against the enquiry.
is_lastBOOLEANIs Last CommunicationTrue for the most recent interaction recorded against the enquiry.
is_schedulingBOOLEANIs Scheduling InteractionTrue when the interaction was about getting an appointment into the diary, as opposed to a reminder or follow-up.
is_last_schedulingBOOLEANIs Last Scheduling InteractionTrue for the most recent scheduling interaction on the enquiry.
count_communicationsINTEGERCommunication CountAlways 1 on every row; SUM to count interactions.

Relationships

Related data martOnCardinalityMeaning
Leadslead_id = lead_idN:1The enquiry this interaction is about.
Patient Access Agentagent_id = agent_idN:1The agent who handled the interaction.
Patientspatient_id = patient_idN:1The patient this interaction was with.

Leads Data Mart

Leads

The moment someone asks for an appointment: one row per inbound enquiry, whether it arrived as a tracked phone call, a website form, a chat, a lead form on an ad platform, a referral from another physician or typed in by staff. Each enquiry carries the location it was directed to, the session that produced it where the two could be matched, and the state it has since reached — still new, being worked, converted into a patient, or one of the two dead ends: never reached, or rejected with a stated reason. This is the only place in the model where the enquiries that were never reached at all sit next to the ones that converted, which is what makes the size of that loss visible rather than assumed.

Fields

ColumnTypeAliasDescription
lead_idSTRINGLead IDPK. Unique identifier for this enquiry.
session_idSTRINGSession IDSession the enquiry was submitted during. NULL when the enquiry began outside the web funnel, such as a physician referral or a direct call. FK to Sessions
patient_idSTRINGPatient IDSet only once the enquiry converts into a patient. NULL until then — use IS NOT NULL for "converted enquiries", which is why a rejected or unreached enquiry is never counted as a patient. FK to Patients
clinic_idSTRINGClinic IDLocation the enquiry was directed to. FK to Clinic
lead_submitted_atTIMESTAMPLead Submitted AtWhen the enquiry was received. The series to plot for enquiry volume.
statusSTRINGStatusHow far the enquiry got: new, in_work, converted, no_answer, rejected.
source_systemSTRINGSource SystemWhat captured the enquiry: call_tracking, google_ads, meta_lead_form, web_form, web_chat, physician_referral, manual.
channel_nameSTRINGChannelHow the patient reached out: phone, web_form, web_chat, patient_portal.
rejection_reasonSTRINGRejection ReasonWhy the enquiry was rejected: insurance_not_accepted, cost_concern, chose_competitor, wrong_number, duplicate, not_interested. NULL unless status = 'rejected'.
is_manual_entryBOOLEANIs Manual EntryTrue when a staff member created the enquiry by hand rather than it being captured automatically.
countrySTRINGCountryCountry the enquiry came from.
created_atTIMESTAMPCreated AtWhen the enquiry record was created in the warehouse.
count_leadsINTEGERLead CountAlways 1 on every row; SUM to count enquiries.

Relationships

Related data martOnCardinalityMeaning
Clinicclinic_id = clinic_idN:1The clinic the enquiry asked about.
Patientspatient_id = patient_idN:1The patient this enquiry was matched to.
Sessionssession_id = session_idN:1The website visit the enquiry came from.

Patient Access Agent Data Mart

Patient Access Agent

The people who answer the phone. Patient access is the function that handles inbound enquiries and books them into a clinician's diary, and since the overwhelming majority of new patients arrive by phone rather than by form, this small team sits directly on the network's growth. One row per agent, with the role they hold — front-line representative, senior representative or supervisor — and the country they work in. On its own the mart is a short staff list; joined to the interaction history it is what makes contact-centre performance answerable at all, because every call, message and booking attempt is attributed to the agent who handled it.

Fields

ColumnTypeAliasDescription
agent_idSTRINGAgent IDPK. Unique identifier for this contact-centre agent.
first_nameSTRINGFirst NameAgent's given name.
last_nameSTRINGLast NameAgent's family name.
roleSTRINGRolePosition on the team: patient_access_rep, senior_rep or supervisor. Use this to ask whether experience changes booking rates.
countrySTRINGCountryTwo-letter country code the agent works in.
emailSTRINGEmail AddressWork email address for the agent.
is_activeBOOLEANIs ActiveTrue while the agent is still on the team.

Patients Data Mart

Patients

Who the network is treating, and what each patient is worth: one row per patient, combining the identity and contact details the clinics hold with the value metrics built on top of their visit history — lifetime value, average value per visit, how many times they have attended, when they first and last came, and how long it has been since. The distinction between a new and an established patient runs through everything here, because the two behave differently on almost every measure that matters, from how much treatment they accept to how likely they are to attend. has_unused_plan and unused_plan_revenue make the recall opportunity explicit: treatment that was planned and paid attention to but never delivered is revenue already earned and still sitting on the table.

Fields

ColumnTypeAliasDescription
patient_idSTRINGPatient IDPK. Unique identifier for this patient.
lead_idSTRINGAcquisition Lead IDEnquiry that first brought this patient in. First touch only — a patient who enquired more than once keeps just the earliest here. Denormalised, not a declared relationship; the full history is in Leads.
session_idSTRINGFirst Session IDWebsite session the patient was first captured in, where one could be matched.
source_ehrSTRINGSource EHRPractice-management system this patient record originated in.
external_ehr_idSTRINGExternal EHR IDIdentifier for this patient in the originating system.
first_nameSTRINGFirst NamePatient's given name.
last_nameSTRINGLast NamePatient's family name.
phoneSTRINGPhone NumberPrimary contact number for the patient.
emailSTRINGEmail AddressPrimary email address for the patient.
date_of_birthDATEDate of BirthPatient's date of birth, the basis for any age banding.
genderSTRINGGenderPatient's recorded gender.
countrySTRINGCountryPatient's country of residence.
languageSTRINGLanguagePreferred language for communication: en, fr or es.
patient_typeSTRINGPatient Typenew for a first-time patient, established for a returning one. Control for this before comparing acceptance, attendance or value.
rfm_labelSTRINGRFM SegmentRecency, frequency and monetary segment: new, promising, loyal, champions, at_risk or lost.
recency_scoreINTEGERRecency ScoreScore for how recently the patient last attended. Kept so segment boundaries can be re-cut.
frequency_scoreINTEGERFrequency ScoreScore for how often the patient attends.
monetary_scoreINTEGERMonetary ScoreScore for how much the patient has spent.
ltvNUMERICLifetime ValueTotal revenue from this patient to date, in USD.
avg_visit_valueNUMERICAverage Visit ValueAverage revenue per attended visit for this patient, in USD.
total_visitsINTEGERTotal VisitsNumber of visits this patient has attended. Reconciles with the attended rows in Visits.
first_visit_dateDATEFirst Visit DateDate of the patient's first visit.
last_visit_dateDATELast Visit DateDate of the patient's most recent visit.
days_since_last_visitINTEGERDays Since Last VisitWhole days between the last visit and the reporting date. The recall trigger.
has_unused_planBOOLEANHas Unused PlanTrue when the patient has planned treatment that has not been delivered.
unused_plan_revenueNUMERICUnused Plan RevenueValue of planned but undelivered treatment for this patient, in USD.
created_atTIMESTAMPCreated AtWhen the patient record was first created.
count_patientsINTEGERPatient CountAlways 1 on every row; SUM to count patients.

Provider Data Mart

Provider

The clinicians patients actually see: one row per provider, with the specialty they practise and the location they practise at. "Provider" is the collective the industry uses for the physicians, nurse practitioners and physician assistants who deliver care, and specialty is what makes this mart load-bearing — it is the only place the model says whether a visit was primary care, dermatology, orthopedics or behavioral health, and those service lines behave nothing alike on revenue per visit or on how often patients fail to turn up. is_active distinguishes clinicians currently practising from those who have left, so a provider who worked two months of the year is not compared against one who worked twelve.

Fields

ColumnTypeAliasDescription
provider_idSTRINGProvider IDPK. Unique identifier for this clinician.
clinic_idSTRINGClinic IDLocation this clinician practises at. FK to Clinic
first_nameSTRINGFirst NameClinician's given name.
last_nameSTRINGLast NameClinician's family name.
specialtySTRINGSpecialtyService line this clinician practises: primary_care, dermatology, orthopedics, cardiology, pediatrics, physiotherapy, behavioral_health. The only path from a visit to its service line.
emailSTRINGEmail AddressWork email address for the clinician.
is_activeBOOLEANIs ActiveTrue while the clinician is still practising at the network. Filter on it before comparing providers, so a mid-period joiner is not read as underproductive.

Relationships

Related data martOnCardinalityMeaning
Clinicclinic_id = clinic_idN:1The clinic the clinician works out of.

Sessions Data Mart

Sessions

Every visit to the website, before anyone has identified themselves: one row per session, with the campaign, keyword and ad creative that brought it, the page it landed on, the device it came from and the city it came from. This is the widest mart in the model and the top of the acquisition funnel — most sessions never become an enquiry, which is the point, because the ratio between sessions and enquiries by landing page and channel is where wasted spend shows up. It also records consent_at, so sessions that predate tracking consent can be separated from those that carry it, and patient_id on the small minority of sessions the network could later tie back to a known patient.

Fields

ColumnTypeAliasDescription
session_idSTRINGSession IDPK. Unique identifier for this website session.
dateDATEDateCalendar date the session occurred. The series to plot for traffic volume. Part of the join grain shared with Attribution. FK to Attribution
sourceSTRINGSourceOrigin of the traffic, such as a search engine or social network. Part of the join grain shared with Attribution. FK to Attribution
mediumSTRINGMediumTraffic type, such as cpc, organic, referral or (none). Part of the join grain shared with Attribution. FK to Attribution
campaignSTRINGCampaignMarketing campaign that produced the session. Part of the join grain shared with Attribution. FK to Attribution
ad_contentSTRINGAd ContentSpecific creative or ad variant the visitor clicked.
ad_groupSTRINGAd GroupAd group within the campaign.
channel_groupingSTRINGChannel GroupingPre-rolled channel classification: Paid Search, Paid Social, Organic Search, Direct, Referral. The default grouping for channel reporting.
keywordSTRINGKeywordSearch term that triggered the ad or organic result.
landing_pageSTRINGLanding PagePage path the visitor first arrived on. Compare enquiry rates across these to find pages that attract volume but convert poorly.
landing_host_nameSTRINGLanding Host NameDomain or subdomain the session started on.
urlSTRINGURLFull web address of the landing page, including protocol and domain.
session_startTIMESTAMPSession StartExact moment the session began.
consent_atTIMESTAMPConsent TimeWhen the visitor granted tracking consent. NULL when no consent was recorded.
client_idSTRINGClient IDBrowser-level identifier, used to tell devices apart.
user_idSTRINGUser IDKnown-user identifier, stable across sessions once the visitor is recognised.
countrySTRINGCountryCountry the session originated from. Part of the join grain shared with Attribution, which is why including it makes each session resolve to exactly one attribution row rather than one per country. FK to Attribution
regionSTRINGRegionState or province the visitor was in.
citySTRINGCityCity the visitor was in.
device_categorySTRINGDevice CategoryHardware used: Mobile, Desktop or Tablet.
is_first_visitor_sessionBOOLEANIs First Visitor SessionTrue when this is the first session recorded for that visitor.
patient_idSTRINGPatient IDAlways NULL. Sessions stay anonymous here by design — resolving one to a patient would need this mart to read Patients, closing a cycle back through Visits and Leads. The linkage lives on Patients.session_id and Leads.session_id instead, so join from those.
count_sessionsINTEGERSession CountAlways 1 on every row; SUM to count sessions.

Relationships

Related data martOnCardinalityMeaning
Attributiondate = date, source = source, medium = medium, campaign = campaign, country = countryN:1The acquisition funnel this visit rolls into.

Visits Data Mart

Visits

Where the network earns: one row per appointment, from the moment it is put in the diary to whatever became of it — attended, cancelled in advance, or simply not turned up to. Keeping cancellations and no-shows apart is deliberate, because they are different problems with different fixes: one gives the clinic a chance to refill the slot and the other does not. Each visit records the clinician who delivered it and the service they delivered, the location and its currency, what it billed both locally and converted, how the patient paid, and where the wider treatment plan stood at the time. It is the only mart that carries revenue, so every question about money starts here.

Fields

ColumnTypeAliasDescription
visit_idSTRINGVisit IDPK. Unique identifier for this appointment.
patient_idSTRINGPatient IDPatient the appointment is for. FK to Patients
lead_idSTRINGLead IDEnquiry that produced this appointment. NULL for established patients who booked without a fresh enquiry. FK to Leads
provider_idSTRINGProvider IDClinician delivering the appointment. FK to Provider
clinic_idSTRINGClinic IDLocation the appointment takes place at. FK to Clinic
visit_dateDATEVisit DateDate the appointment is scheduled for.
visit_timeTIMEVisit TimeTime of day the appointment is scheduled for.
visit_typeSTRINGVisit Typenew_patient_visit for a patient's first appointment, follow_up for a subsequent one.
statusSTRINGStatusWhat became of the appointment: scheduled, completed, cancelled (called off in advance) or no_show (not attended, no notice). The last two are distinct because only a cancellation lets the slot be refilled.
service_nameSTRINGServiceService delivered at the appointment.
treatment_plan_statusSTRINGTreatment Plan StatusWhere the patient's wider course of treatment stood: none, active, partial, completed, unused.
revenue_localNUMERICRevenue (Local)Revenue billed, in the location's own currency. Only attended visits carry revenue.
currencySTRINGLocal CurrencyCurrency revenue_local is denominated in: USD or CAD.
revenue_normalizedNUMERICRevenue (USD)Revenue converted to USD. Use this for any figure spanning both markets.
fx_rate_to_usdNUMERICFX Rate to USDRate used to convert revenue_local into revenue_normalized.
payment_methodSTRINGPayment MethodHow the visit was settled: insurance, card, cash, online, payment_plan.
created_atTIMESTAMPCreated AtWhen the appointment was booked. Compare against visit_date for booking lead time.
is_first_visitBOOLEANIs First VisitTrue when this is the patient's first ever attendance at the network.
count_visitsINTEGERVisit CountAlways 1 on every row; SUM to count appointments.

Relationships

Related data martOnCardinalityMeaning
Clinicclinic_id = clinic_idN:1The clinic where the visit happened.
Leadslead_id = lead_idN:1The enquiry this appointment came from.
Patientspatient_id = patient_idN:1The patient who attended.
Providerprovider_id = provider_idN:1The clinician who saw the patient.

Apply to your project

  1. 1

    Install the Import Model plugin

    One plugin, installed once, in your own OWOX workspace.

    Get the plugin →

  2. 2

    Import this model

    Point it at this bundle and it creates every data mart above, joins and all.

    Open the model →

  3. 3

    Plug in your data and destinations

    Connect your own sources and send the results where your team already works.

    Browse connectors →

4. Optional — customize as you wish. Rename a column, drop a mart, add your own: once it is imported it is yours, and nothing here syncs back.