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

# Demographic Performance → Persona Refinement

> Pull gender and age conversion data from Google Ads to create data-backed Mavera personas calibrated to your real buyers.

### Scenario

Your Google Ads campaigns expose which age ranges and genders actually convert — not who you *think* your buyer is, but who Google *proves* is buying. You pull `gender_view` and `age_range_view` with conversion metrics, then create or update Mavera personas that match your real converting demographics. The result is a persona library calibrated to paid data.

### Architecture

```mermaid theme={"dark"}
flowchart LR
    A["Google Ads GAQL (gender_view + age_range_view)"] --> B[Conversion-weighted segments] --> C["POST /api/v1/personas"] --> D[Data-backed persona library]
```

### Code

<CodeGroup>
  ```python Python theme={"dark"}
  import os, requests, time
  from google.ads.googleads.client import GoogleAdsClient

  MV = os.environ["MAVERA_API_KEY"]
  CUSTOMER_ID = os.environ["GOOGLE_ADS_CUSTOMER_ID"]
  MB = "https://app.mavera.io/api/v1"
  MH = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}

  client = GoogleAdsClient.load_from_env()
  ga_service = client.get_service("GoogleAdsService")

  gender_query = """
      SELECT
          ad_group_criterion.gender.type,
          metrics.impressions, metrics.clicks, metrics.conversions,
          metrics.cost_micros, metrics.conversions_value
      FROM gender_view
      WHERE segments.date DURING LAST_90_DAYS
      ORDER BY metrics.conversions DESC
  """

  age_query = """
      SELECT
          ad_group_criterion.age_range.type,
          metrics.impressions, metrics.clicks, metrics.conversions,
          metrics.cost_micros, metrics.conversions_value
      FROM age_range_view
      WHERE segments.date DURING LAST_90_DAYS
      ORDER BY metrics.conversions DESC
  """

  gender_data = {}
  for row in ga_service.search(customer_id=CUSTOMER_ID, query=gender_query):
      g = row.ad_group_criterion.gender.type_.name
      gender_data.setdefault(g, {"impressions": 0, "clicks": 0, "conversions": 0, "value": 0})
      gender_data[g]["impressions"] += row.metrics.impressions
      gender_data[g]["clicks"] += row.metrics.clicks
      gender_data[g]["conversions"] += row.metrics.conversions
      gender_data[g]["value"] += row.metrics.conversions_value

  age_data = {}
  for row in ga_service.search(customer_id=CUSTOMER_ID, query=age_query):
      a = row.ad_group_criterion.age_range.type_.name
      age_data.setdefault(a, {"impressions": 0, "clicks": 0, "conversions": 0, "value": 0})
      age_data[a]["impressions"] += row.metrics.impressions
      age_data[a]["clicks"] += row.metrics.clicks
      age_data[a]["conversions"] += row.metrics.conversions
      age_data[a]["value"] += row.metrics.conversions_value

  total_conv = sum(d["conversions"] for d in age_data.values()) or 1

  top_ages = sorted(age_data.items(), key=lambda x: x[1]["conversions"], reverse=True)[:3]
  top_gender = sorted(gender_data.items(), key=lambda x: x[1]["conversions"], reverse=True)[:2]

  created = []
  for age_name, age_metrics in top_ages:
      for gender_name, gender_metrics in top_gender:
          conv_share = (age_metrics["conversions"] + gender_metrics["conversions"]) / (2 * total_conv)
          if conv_share < 0.05:
              continue

          cpa = ((age_metrics.get("cost_micros", 0) or 0) / 1_000_000) / max(age_metrics["conversions"], 1)
          persona = requests.post(f"{MB}/personas", headers=MH, json={
              "name": f"Google Ads: {gender_name.replace('_', ' ').title()} {age_name.replace('AGE_RANGE_', '').replace('_', '-')}",
              "description": (
                  f"Data-backed persona from Google Ads (last 90 days). "
                  f"Age: {age_name.replace('AGE_RANGE_', '').replace('_', '-')}. Gender: {gender_name}. "
                  f"Conversions: {age_metrics['conversions']:.0f} ({age_metrics['conversions']/total_conv:.0%} of total). "
                  f"CPA: ${cpa:.2f}. Conv value: ${age_metrics['value']:.0f}."
              ),
              "demographic": {
                  "age_range": age_name.replace("AGE_RANGE_", "").replace("_", "-"),
                  "gender": gender_name.lower(),
              },
          }).json()
          created.append({"name": persona.get("name"), "id": persona["id"]})
          time.sleep(0.3)

  print(f"Created {len(created)} demographic personas")
  for p in created:
      print(f"  {p['name']} → {p['id']}")
  ```

  ```javascript JavaScript theme={"dark"}
  const DEV_TOKEN = process.env.GOOGLE_ADS_DEVELOPER_TOKEN;
  const ACCESS_TOKEN = process.env.GOOGLE_ADS_ACCESS_TOKEN;
  const CUSTOMER_ID = process.env.GOOGLE_ADS_CUSTOMER_ID;
  const MV = process.env.MAVERA_API_KEY;
  const MB = "https://app.mavera.io/api/v1";
  const MH = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };

  async function gaQuery(query) {
    const res = await fetch(
      `https://googleads.googleapis.com/v23/customers/${CUSTOMER_ID}/googleAds:searchStream`,
      {
        method: "POST",
        headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, "developer-token": DEV_TOKEN, "Content-Type": "application/json" },
        body: JSON.stringify({ query }),
      }
    ).then((r) => r.json());
    return res[0]?.results || [];
  }

  const genderRows = await gaQuery(`
    SELECT ad_group_criterion.gender.type, metrics.impressions, metrics.clicks,
      metrics.conversions, metrics.cost_micros, metrics.conversions_value
    FROM gender_view WHERE segments.date DURING LAST_90_DAYS ORDER BY metrics.conversions DESC`);

  const ageRows = await gaQuery(`
    SELECT ad_group_criterion.age_range.type, metrics.impressions, metrics.clicks,
      metrics.conversions, metrics.cost_micros, metrics.conversions_value
    FROM age_range_view WHERE segments.date DURING LAST_90_DAYS ORDER BY metrics.conversions DESC`);

  function aggregate(rows, field) {
    const data = {};
    for (const row of rows) {
      const key = field(row);
      data[key] ??= { impressions: 0, clicks: 0, conversions: 0, value: 0 };
      data[key].impressions += parseInt(row.metrics.impressions);
      data[key].clicks += parseInt(row.metrics.clicks);
      data[key].conversions += parseFloat(row.metrics.conversions);
      data[key].value += parseFloat(row.metrics.conversionsValue || "0");
    }
    return data;
  }

  const genderData = aggregate(genderRows, (r) => r.adGroupCriterion.gender.type);
  const ageData = aggregate(ageRows, (r) => r.adGroupCriterion.ageRange.type);

  const totalConv = Object.values(ageData).reduce((s, d) => s + d.conversions, 0) || 1;
  const topAges = Object.entries(ageData).sort(([, a], [, b]) => b.conversions - a.conversions).slice(0, 3);
  const topGenders = Object.entries(genderData).sort(([, a], [, b]) => b.conversions - a.conversions).slice(0, 2);

  const created = [];
  for (const [ageName, ageM] of topAges) {
    for (const [genderName, genderM] of topGenders) {
      const convShare = (ageM.conversions + genderM.conversions) / (2 * totalConv);
      if (convShare < 0.05) continue;

      const label = `${genderName.replace(/_/g, " ")} ${ageName.replace("AGE_RANGE_", "").replace(/_/g, "-")}`;
      const p = await fetch(`${MB}/personas`, {
        method: "POST", headers: MH,
        body: JSON.stringify({
          name: `Google Ads: ${label}`,
          description: `Data-backed persona (90d). Age: ${ageName}. Gender: ${genderName}. Conv: ${ageM.conversions.toFixed(0)} (${(ageM.conversions / totalConv * 100).toFixed(0)}%).`,
          demographic: { age_range: ageName.replace("AGE_RANGE_", "").replace(/_/g, "-"), gender: genderName.toLowerCase() },
        }),
      }).then((r) => r.json());
      created.push({ name: `Google Ads: ${label}`, id: p.id });
      await new Promise((r) => setTimeout(r, 300));
    }
  }

  console.log(`Created ${created.length} demographic personas`);
  created.forEach((p) => console.log(`  ${p.name} → ${p.id}`));
  ```
</CodeGroup>

### Example Output

```json theme={"dark"}
{
  "created": 4,
  "personas": [
    { "name": "Google Ads: Male 25-34", "id": "per_gads_m25", "conv_share": "34%", "cpa": "$12.40" },
    { "name": "Google Ads: Female 25-34", "id": "per_gads_f25", "conv_share": "22%", "cpa": "$15.80" },
    { "name": "Google Ads: Male 35-44", "id": "per_gads_m35", "conv_share": "18%", "cpa": "$18.20" },
    { "name": "Google Ads: Female 35-44", "id": "per_gads_f35", "conv_share": "12%", "cpa": "$21.50" }
  ]
}
```

### Error Handling

<AccordionGroup>
  <Accordion title="UNDETERMINED gender/age">Google can't classify all users. `UNDETERMINED` often has high volume. Exclude it from persona creation but monitor its conversion share.</Accordion>
  <Accordion title="Low conversion counts">With fewer than 30 conversions per segment, data is noisy. Use 90-day windows or aggregate across campaigns for statistical significance.</Accordion>
  <Accordion title="Persona duplicates on re-run">Check existing personas with `GET /api/v1/personas?search=Google Ads` before creating. Use `PATCH` to update demographics instead.</Accordion>
</AccordionGroup>

***

<CardGroup cols={2}>
  <Card title="All Google Ads jobs" icon="rectangle-history" href="/integrations/google-ads">
    View all 7 Google Ads integration jobs
  </Card>

  <Card title="Personas API" icon="users" href="/api-reference/personas">
    Full reference for POST /api/v1/personas
  </Card>
</CardGroup>
