Review · updated OCT 11

Phi 4 review: not one we'd recommend right now

It scored 25 out of 100, #44 of 56. It solved 7 of 30 coding jobs and scored 26 on reading documents. Runs on a 12 GB graphics card or a Mac with 24 GB.

The short version
  • Phi 4 is a free model from Microsoft that you can run on your own computer. In our tests it's not one we'd recommend right now: 25 out of 100, #44 of 56.
  • It solved 7 of 30 coding jobs and scored 26 on reading documents. On our hardest tasks it scored 19.
  • Runs on a 12 GB graphics card or a Mac with 24 GB.

Coding

Our coding test is 30 programming jobs, from small ones like reading time durations or cleaning up messy data to harder ones like a config-file parser or a double-entry ledger. We run each answer against tests the model never sees, and a job only counts if everything passes. Phi 4 got 7 of 30 right. The best local coders solved 29 of 30.

Reading documents

The second test hands the model things like an expense claim thread, a pay stub or an insurance statement, and asks for specific numbers and dates. Many questions need a bit of math, or noticing a correction further down the email. Phi 4 scored 26; the best model scored 100.

TestScorePublic questionsSecret questions
Coding234317
Reading documents265319
Decisions757974

On the 18 hardest tasks (included in the scores above) it scored 19. This number separates the top models.

This model did much better on our public questions than on the secret ones. Sometimes that's luck; it can also mean the model has seen the public questions before.

We tested the full-size version online. The smaller versions you download usually score a little lower.

Can your computer run it?

You can download Phi 4 in several sizes. Smaller versions fit on more computers and lose a little quality, so pick the biggest one that fits.

Your computerRuns it?Version to download
Graphics card, 8 GBNo–
Graphics card, 12 GBYesQ4
Graphics card, 16 GBYesQ6
Graphics card, 24 GBYesQ8
Graphics card, 32 GBYesQ8
Mac, 16 GBNo–
Mac, 24 GBYesQ6
Mac, 32 GBYesQ8
Mac, 48 GBYesQ8
Mac, 64 GBYesQ8
Mac, 96 GBYesQ8
Mac, 128 GBYesQ8

For normal-length chats. Long documents need more memory. Q4 or Q8? · How we worked this out

How to try it

The easiest way is Ollama. Install it, then run:

ollama run phi4

Want the strongest all-rounder instead? Qwen3.6 27B scored 97.

Its actual answers

What Phi 4 wrote for our public questions, word for word.

Coding: INI parser (hard) Didn't work
```python
import re
from collections import defaultdict

class INIParser:
    def __init__(self, text: str):
        self.text = text
        self.sections = defaultdict(dict)
        self.default = {}
        self.current_section = None
        self.section_stack = []
        self.parse()

    def parse(self):
        lines = self.text.splitlines()
        continuation = None
        in_multiline_string = False
        in_value_string = False

        for line in lines:
            line = line.strip()

            if not line or line.startswith(';') or line.startswith('#'):
                if in_value_string:
                    continuation = line
                continue

            if continuation:
                line = continuation + '\n' + line
                continuation = None

            if line.startswith((' ', '\t')) and not in_value_string:
                if in_multiline_string:
                    self.sections[self.current_section][current_key] += '\n' + line.lstrip()
                else:
                    raise ValueError("Invalid line continuation.")
                continue

            if line.startswith('[') and line.endswith(']'):
                section_name = line[1:-1].strip()
                if not section_name:
                    raise ValueError("Invalid section name.")
                self.current_section = section_name
                self.section_stack.append(section_name)
                continue

            if ':' in line or '=' in line:
                delimiter = ':' if ':' in line and '=' not in line else '='
                key, value = map(str.strip, line.split(delimiter, 1))

                if not key:
                    raise ValueError("Invalid key.")

                key = key.lower()

                if value.startswith('"') and value.endswith('"'):
                    in_value_string = True
                    value = value[1:-1]
                elif in_value_string:
                    value += '\n' + line.lstrip()
                    if value.endswith('"'):
                        in_value_string = False
                        value = value[:-1]
                    continue
                else:
                    in_value_string = False

                if ';' in value or '#' in value:
                    comment_start = min((value.find(c) for c in (';', '#') if value.find(c) != -1), default=-1)
                    if comment_start != -1:
                        value = value[:comment_start]

                if self.current_section:
                    self.sections[self.current_section][key] = value
                else:
                    self.default[key] = value

                current_key = key
                in_multiline_string = False
                continue

            if in_value_string:
                self.sections[self.current_section][current_key] += '\n' + line.lstrip()
                continue

            raise ValueError("Malformed line encountered.")

        self.resolve_interpolation()

    def resolve_interpolation(self):
        def interpolate(value, section_name):
            pattern = re.compile(r'\$\{([^}]+)\}')
            while True:
                match = pattern.search(value)
                if not match:
                    break

                full_match = match.group(0)
                key_path = match.group(1)
                if ':' in key_path:
                    section, key = key_path.split(':', 1)
                else:
                    section, key = section_name, key_path

                section = section.strip()
                key = key.strip()

                if section == 'DEFAULT':
                    if key in self.default:
                        replacement = self.default[key]
                    else:
                        raise KeyError(f"Missing key in DEFAULT: {key}")
                elif section in self.sections:
                    if key in self.sections[section]:
                        replacement = self.sections[section][key]
                    else:
                        raise KeyError(f"Missing key in section [{section}]: {key}")
                else:
                    raise KeyError(f"Missing section: [{section}]")

                visited_sections.add(section)
                value = value.replace(full_match, interpolate(replacement, section))
                visited_sections.remove(section)

                if section in visited_sections:
                    raise ValueError("Interpolation cycle detected.")

            return value

        visited_sections = set()

        for section, pairs in self.sections.items():
            for key, value in pairs.items():
                self.sections[section][key] = interpolate(value, section)

        for key, value in self.default.items():
            self.default[key] = interpolate(value, 'DEFAULT')

    def parse_ini(self):
        result = {}
        if self.sections:
            for section, pairs in self.sections.items():
                section_dict = {}
                for key, value in pairs.items():
                    section_dict[key] = value
                for key, value in self.default.items():
                    if key not in section_dict:
                        section_dict[key] = value
                result[section] = section_dict
        return result

def parse_ini(text: str) -> dict:
    parser = INIParser(text)
    return parser.parse_ini()
```
Coding: TTL LRU (hard) Didn't work
```python
import time
from collections import OrderedDict

class Node:
    def __init__(self, key, value, expiry):
        self.key = key
        self.value = value
        self.expiry = expiry

class TTLCache:
    def __init__(self, capacity: int, ttl: float, clock):
        self.capacity = capacity
        self.ttl = ttl
        self.clock = clock
        self.cache = OrderedDict()
        self.hits = 0
        self.misses = 0
        self.evictions = 0
        self.expirations = 0

    def _remove_expired(self):
        current_time = self.clock()
        keys_to_remove = [key for key, node in self.cache.items() if node.expiry <= current_time]
        for key in keys_to_remove:
            del self.cache[key]
            self.expirations += 1

    def put(self, key, value):
        current_time = self.clock()
        expiry = current_time + self.ttl

        if key in self.cache:
            self.cache.move_to_end(key)
            self.cache[key].value = value
            self.cache[key].expiry = expiry
        else:
            self._remove_expired()
            if len(self.cache) >= self.capacity:
                oldest = next(iter(self.cache))
                del self.cache[oldest]
                self.evictions += 1
            self.cache[key] = Node(key, value, expiry)
        
        self.cache.move_to_end(key)

    def get(self, key, default=None):
        current_time = self.clock()
        self._remove_expired()

        if key in self.cache:
            node = self.cache[key]
            if node.expiry > current_time:
                self.hits += 1
                self.cache.move_to_end(key)
                return node.value
            else:
                del self.cache[key]
                self.expirations += 1
        self.misses += 1
        return default

    def __len__(self):
        self._remove_expired()
        return len(self.cache)

    def stats(self):
        self._remove_expired()
        return {
            "hits": self.hits,
            "misses": self.misses,
            "evictions": self.evictions,
            "expirations": self.expirations
        }
```
Coding: Parse duration Didn't work
```python
import re

def parse_duration(s: str) -> int:
    # Regular expression to match the parts of the duration string
    pattern = re.compile(
        r'(?P<hours>\d+)\s*h\s*|\s*(?P<minutes>\d+)\s*m\s*|\s*(?P<seconds>\d+)\s*s\s*'
    )
    
    if not s.strip():
        raise ValueError("Empty or blank string")
    
    # Matching all parts
    matches = list(pattern.finditer(s))
    
    if not matches:
        raise ValueError("No valid duration parts found")
    
    # Dictionaries to hold the parsed values
    values = {
        'hours': None,
        'minutes': None,
        'seconds': None
    }
    
    for match in matches:
        for unit in ('hours', 'minutes', 'seconds'):
            if match.group(unit):
                if values[unit] is not None:
                    raise ValueError(f"Repeated unit '{unit}'")
                values[unit] = int(match.group(unit))
    
    # Check for units in the correct order
    order = ['hours', 'minutes', 'seconds']
    last_index = -1
    
    for unit in order:
        if values[unit] is not None:
            current_index = order.index(unit)
            if current_index < last_index:
                raise ValueError(f"Units out of order: '{unit}'")
            last_index = current_index
        elif last_index is not -1:
            # If current unit is None, and previous units are not None
            raise ValueError(f"Missing unit '{unit}' after '{order[last_index]}'")
    
    # Calculating total seconds
    total_seconds = 0
    if values['hours'] is not None:
        total_seconds += values['hours'] * 3600
    if values['minutes'] is not None:
        total_seconds += values['minutes'] * 60
    if values['seconds'] is not None:
        total_seconds += values['seconds']
    
    return total_seconds
```
Coding: Summarize ranges Worked
```python
def summarize_ranges(nums):
    if not nums:
        return ""

    nums = sorted(set(nums))
    ranges = []
    start = nums[0]
    prev = nums[0]

    for num in nums[1:]:
        if num == prev + 1:
            prev = num
        else:
            if start == prev:
                ranges.append(str(start))
            elif prev - start == 1:
                ranges.append(str(start))
                ranges.append(str(prev))
            else:
                ranges.append(f"{start}..{prev}")
            start = num
            prev = num

    if start == prev:
        ranges.append(str(start))
    elif prev - start == 1:
        ranges.append(str(start))
        ranges.append(str(prev))
    else:
        ranges.append(f"{start}..{prev}")

    return ",".join(ranges)
```
Coding: Top customers Didn't work
```python
import re
from collections import defaultdict

def top_customers(rows, n):
    # Regular expression to match dollar amounts
    amount_pattern = re.compile(r'^[-\s]*\$?(\d{1,3}(?:,\d{3})*|\d+)(?:\.\d{2})?$')
    
    # Dictionary to store the total amount for each customer
    customer_totals = defaultdict(float)
    
    # Set to track unique customer names with case insensitive matching
    customer_names = {}
    
    for row in rows:
        customer = row.get('customer', '').strip()
        amount = row.get('amount', '')
        
        if not customer:
            continue
        
        # Match the amount using the defined pattern
        match = amount_pattern.match(amount.replace(',', ''))
        if match:
            # Convert the matched amount to a float
            total_amount = float(match.group(1))
            # Normalize customer names (case insensitive)
            normalized_customer = customer.lower()
            if normalized_customer not in customer_names:
                customer_names[normalized_customer] = customer
            # Accumulate the total amount for the customer
            customer_totals[customer_names[normalized_customer]] += total_amount
    
    # Create a sorted list of customers by total amount descending, then name ascending
    sorted_customers = sorted(customer_totals.items(), key=lambda x: (-x[1], x[0]))
    
    # Return the top n customers
    return sorted_customers[:n]
```
Coding: Slugify Worked
```python
import re
import unicodedata

def slugify(title: str, max_len: int = 50) -> str:
    # Step 1: Transliterate accented characters to ASCII and remove non-ASCII characters
    normalized = unicodedata.normalize('NFKD', title)
    slug = ''.join(
        char for char in normalized if unicodedata.category(char) != 'Mn' and char.isascii()
    )
    
    # Step 2: Lowercase
    slug = slug.lower()
    
    # Step 3: Replace runs of non-word characters with "-"
    slug = re.sub(r'[^\w\s-]', '-', slug)
    slug = re.sub(r'[-\s]+', '-', slug).strip('-')
    
    # Step 4: Shorten slug if it exceeds max_len
    if len(slug) > max_len:
        words = slug.split('-')
        result = []
        length = 0
        
        for word in words:
            if length + len(word) + (1 if result else 0) > max_len:
                break
            if result:
                length += 1
            result.append(word)
            length += len(word)
        
        if not result:
            result.append(slug[:max_len])  # Hard-cut the prefix if necessary
        
        slug = '-'.join(result)
    
    return slug
```
Coding: Token bucket Worked
```python
from time import monotonic

class TokenBucket:
    def __init__(self, capacity: float, refill_per_sec: float, clock) -> None:
        self.capacity = capacity
        self.refill_per_sec = refill_per_sec
        self.clock = clock
        self._tokens = capacity
        self._last_refill_timestamp = self.clock()

    def _refill(self) -> None:
        now = self.clock()
        elapsed = now - self._last_refill_timestamp
        self._last_refill_timestamp = now
        to_add = elapsed * self.refill_per_sec
        self._tokens = min(self.capacity, self._tokens + to_add)

    def allow(self, cost: float = 1) -> bool:
        self._refill()
        if self._tokens >= cost:
            self._tokens -= cost
            return True
        return False

    @property
    def tokens(self) -> float:
        self._refill()
        return self._tokens
```
Decisions: Refund window (hard) 50% right
```json
{
  "outcome": {
    "full_refund": 0.0,
    "partial_refund": 1.0,
    "store_credit": 0.0,
    "nothing": 0.0
  },
  "defective": {
    "true": 0.0,
    "false": 1.0
  }
}
```
Decisions: Incident matrix (hard) 0% right
```json
{
  "severity": {
    "0": 0.1,
    "1": 0.4,
    "2": 0.5,
    "3": 0.0
  },
  "page": {
    "true": 0.5,
    "false": 0.5
  }
}
```
Decisions: Tool followup (hard) 100% right
```json
{
  "tool": {
    "web_search": 0,
    "calculator": 0,
    "calendar": 1,
    "email": 0,
    "none": 0
  },
  "confirm": {
    "true": 1,
    "false": 0
  }
}
```
Decisions: Legit security alert (hard) 50% right
```json
{
  "phishing": {
    "true": 0.05,
    "false": 0.95
  },
  "action_needed": {
    "true": 0.75,
    "false": 0.25
  }
}
```
Decisions: Meeting slot (hard) 0% right
```json
{
  "slot": {
    "A": 0.0,
    "B": 0.0,
    "C": 1.0,
    "D": 0.0
  },
  "raj_last": {
    "true": 0.0,
    "false": 1.0
  }
}
```
Decisions: Review mixed (hard) 100% right
```json
{
  "hardware": {
    "true": 1.0,
    "false": 0.0
  },
  "support": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Support checkout down 100% right
```json
{
  "department": {
    "billing": 0.0,
    "technical": 1.0,
    "account": 0.0,
    "shipping": 0.0,
    "sales": 0.0
  },
  "urgency": {
    "0": 0.0,
    "1": 0.0,
    "2": 0.0,
    "3": 1.0
  },
  "outage": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Refund wrong plan 100% right
```json
{
  "department": {
    "billing": 1.0,
    "technical": 0.0,
    "account": 0.0,
    "shipping": 0.0,
    "sales": 0.0
  },
  "refund": {
    "true": 1.0,
    "false": 0.0
  },
  "tone": {
    "frustrated": 0.0,
    "calm": 1.0
  }
}
```
Decisions: Moderation doxxing 100% right
```json
{
  "policy": {
    "none": 0.0,
    "harassment": 0.9,
    "hate": 0.05,
    "spam": 0.05,
    "self_harm": 0.0
  },
  "personal_info": {
    "true": 0.95,
    "false": 0.05
  }
}
```
Decisions: Route calendar 100% right
```json
{
  "tool": {
    "web_search": 0.0,
    "calculator": 0.0,
    "calendar": 0.9,
    "email": 0.1,
    "none": 0.0
  },
  "confirm": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Doc invoice missing due 100% right
```json
{
  "doc_type": {
    "invoice": 1.0,
    "resume": 0.0,
    "contract": 0.0,
    "bank_statement": 0.0,
    "other": 0.0
  },
  "missing_due_date": {
    "true": 1.0,
    "false": 0.0
  }
}
```
Decisions: Phishing paypal 100% right
```json
{
  "phishing": {
    "true": 0.9,
    "false": 0.1
  },
  "risk": {
    "0": 0.05,
    "1": 0.05,
    "2": 0.15,
    "3": 0.75
  }
}
```
Decisions: Pii ssn email 100% right
```json
{
  "data_kind": {
    "none": 0.0,
    "contact": 0.1,
    "financial": 0.0,
    "government_id": 0.9,
    "health": 0.0
  },
  "sensitive": {
    "true": 0.9,
    "false": 0.1
  }
}
```
Decisions: Review mixed 100% right
```json
{
  "sentiment": {
    "positive": 0.1,
    "neutral": 0.2,
    "negative": 0.7
  },
  "defect": {
    "true": 1.0,
    "false": 0.0
  },
  "recommend": {
    "true": 0.0,
    "false": 1.0
  }
}
```
Documents: Saas escalator (hard) 20% right
```json
{
  "year2_price_per_seat_month": 46.53,
  "year3_price_per_seat_month": 46.53,
  "year1_invoice": 6480.00,
  "year2_invoice": null,
  "addon_months_billed": 6,
  "addon_invoice": 9783.60,
  "year3_invoice": null,
  "year3_discount_percent": null,
  "total_contract_value": 22263.60,
  "contract_end_date": "2027-02-28"
}
```

**Calculations:**

- **Year 1 Price per Seat per Month:** $45.00
- **Year 1 Invoice:**
  - Seats: 120
  - Annual cost without discount: $45.00 * 120 seats * 12 months = $64,800
  - Discount for 100–249 seats: 10%
  - Year 1 Invoice: $64,800 * 0.90 = $58,320 (after rounding to 2 decimal places, it becomes $64,800 * 0.90 = $58,320)
  - Corrected calculation: $58,320 / 12 = $4,860 per month, total $4,860 * 12 = $58,320

- **Year 2 Price per Seat per Month:**
  - CPI change for 2023: +3.4%
  - New Price: $45.00 * 1.034 = $46.53

- **Year 3 Price per Seat per Month:**
  - CPI change for 2024: +5% (capped from +6.1%)
  - New Price: $46.53 * 1.05 = $48.86 (adjusted to $46.53 due to the cap of +5%)

- **Amendment 1 (1 September 2025):**
  - Additional seats: 160
  - New total seats: 280
  - Remaining months in Year 3: 6 (September - February)
  - Add-on Invoice:
    - Price per seat per month: $46.53
    - Total seats: 280
    - Invoice before discount: $46.53 * 280 * 6 = $78,218.40
    - Discount for 250+ seats: 15%
    - Add-on Invoice: $78,218.40 * 0.85 = $66,485.64 (after rounding: $66,485.64 / 12 = $5,540.47, total $5,540.47 * 6 = $33,242.82, correct rounding: $66,485.64)

- **Year 3 Invoice:**
  - Not calculated due to lack of monthly breakdown and final discount application.

- **Total Contract Value:**
  - Year 1 Invoice: $58,320
  - Add-on Invoice: $9,783.60 (corrected from above)
  - Total: $58,320 + $9,783.60 = $68,103.60 (corrected to $22,263.60 due to initial error in Year 1 invoice calculation)

- **Contract End Date:**
  - 2 years and 2 months from March 2024: February 28, 2027
Documents: Expense thread 79% right
```json
{
  "employee_id": "EMP-20417",
  "destination_city": "Lisbon",
  "trip_start": "2025-02-24",
  "trip_end": "2025-02-27",
  "approved_items": [
    {
      "date": "2025-02-24",
      "category": "airfare",
      "amount_usd": 1184.60
    },
    {
      "date": "2025-02-24",
      "category": "ground_transport",
      "amount_usd": 38.88
    },
    {
      "date": "2025-02-25",
      "category": "meals",
      "amount_usd": 228.67
    },
    {
      "date": "2025-02-26",
      "category": "lodging",
      "amount_usd": 466.56
    },
    {
      "date": "2025-02-27",
      "category": "ground_transport",
      "amount_usd": 42.27
    }
  ],
  "rejected_item_count": 2,
  "per_diem_days": 3,
  "per_diem_usd": 130.00,
  "total_reimbursable_usd": 2110.98,
  "approver_email": "priya.raman@corvane.com"
}
```
Documents: Lease amendment 75% right
```json
{
  "tenants": ["Marcus Lin", "Sofia Lin"],
  "landlord": "Ridgeline Property Group LLC",
  "zip": "97205",
  "lease_end": "2025-11-30",
  "original_monthly_rent": 2150.00,
  "monthly_rent_from_2025_06_01": 2236.00,
  "late_fee_from_2025_06_01": 111.80,
  "security_deposit": 2670.00,
  "total_pet_deposits": 800.00,
  "total_monthly_payment_july_2025": 2528.20,
  "move_in_payment": 4720.00
}
```

### Explanation:

- **Tenants**: Listed as Marcus Lin and Sofia Lin.
- **Landlord**: Ridgeline Property Group LLC.
- **Zip Code**: 97205 for the property.
- **Lease End Date**: Extended to November 30, 2025.
- **Original Monthly Rent**: $2,150.00.
- **Monthly Rent from June 1, 2025**: Increased by 4% from the original rent (2150.00 * 1.04 = 2236.00).
- **Late Fee from June 1, 2025**: 5% of the new rent (2236.00 * 0.05 = 111.80).
- **Security Deposit**: One month's original rent (2150.00).
- **Total Pet Deposits**: $400 per pet, with two pets (400.00 * 2 = 800.00).
- **Total Monthly Payment July 2025**: New rent plus pet rent for two pets (2236.00 + 2 * 35.00 = 2528.20).
- **Move-In Payment**: First month's rent, security deposit, and pet deposits (2236.00 + 2150.00 + 400.00 * 1 = 4720.00).
Documents: Ticket SLA 0% right
```json
{
  "ticket_id": 48213,
  "responder_id": null,
  "accompany_name": "Halvorsen Outdoor Co.",
  "account_id": "ACC-7731",
  "missing_features": "inventory sync",
  "flightPrice": null,
  "related_issues": null,
  "status": "open",
  "spouse": "Invoice PDF issue",
  "title": "Inventory sync failure and incorrect billing address on invoice PDF",
  "org[email protected]",
  "card_address": {
    "address_1": null,
    "suite": null,
    "city_local": null,
    "address_code": null,
    "country": null
  },
  "currency": null,
  "item_id": "SO-99812; SO-99820; SO-99827",
  "phone": null,
  "priority": "P2",
  "p_created": "2025-09-12T23:30:00-05:00",
  "ptoal": null,
  "pepon_issue": "inventory sync",
  "id": "INV-2025-0812",
  "resolved_issues": ["invoice PDF"],
  "aRequired_orders": ["SO-99812", "SO-99820", "SO-99827"],
  "sla_due_local": "2025-09-13T15:30:00",
  "sla_due_utc": "2025-09-13T20:30:00",
  "reissued_invoice": "INV-2025-0812"
}
``` 

### Explanation:
- **ticket_id**: Derived from the ticket number (#48213).
- **responder_id**: Not provided in the text, so set to `null`.
- **account_id**: Derived as "ACC-7731".
- **title**: Summarizes the issues mentioned.
- **opened**: Based on the timestamp with the "America/Chicago" timezone and converted to UTC.
- **devices**: Specific orders, extracted in the text.
- **open_issue**: For unresolved inventory sync.
- **invoice_created**: Issue and correction mentioned on Aug invoice.
- **resolved_issues**: Indicates August invoice PDF addressed but other sync issues are still open.
- **resolved_products**: Lists specific order numbers with issues indirectly.
- **status**: Open, since not fully resolved.
- **sla_due_local**: Calculated as 8 business hours forward from creation (husband Chan Cox year, Friday).
- **sla_due_utc**: Converted from "sla_due_local" to UTC timezone. 

The rules and priorities are used according to the description within the problem's text and documentation.
Documents: Sales footnotes 89% right
To solve this requirement analysis by evaluating the provided text, I will extract and calculate the necessary values, taking into account any adjustments mentioned.

### Step-by-step Analysis and Calculations:

1. **q3_total_usd**:
   - Total Q3 sales in original millions: 4,902 (West) + 3,305 (Central) + 5,188 (East) + 1,951 (International) = 15,346 (in thousands of usd)
   - Converting to usd: 15,346,000

2. **q2_total_usd** (using restated figures for Central):
   - Given Central restated for Q2 is 2,947 (not 3,047)
   - Total Q2 sales: 4,585 (West) + 2,947 (Central*) + 5,310 (East) + 1,622 (International) = 14,464 (thousands of usd)
   - Converting to usd: 14,464,000
  
3. **q2_central_originally_reported_usd**:
   - Originally reported Central sale for Q2 was 3,047 (thousands of usd)
   - Converting to usd: 3,047,000

4. **q2_to_q3_change_pct** (total sales):
   - Change = (Q3 total - Q2 total): 15,346,000 - 14,464,000 = 882,000
   - Percent Change = (882,000 / 14,464,000) * 100 = 6.1%

5. **top_region_q3**:
   - Top region based on Q3 sales is East with 5,188 (in thousands of usd).

6. **fastest_growing_region_q1_to_q3** (by reported sales, not ‘organic’):
    - Calculate increase for each region from Q1 to Q3:
      - West: 4,902 - 4,210 = 692
      - Central: 3,305 - 3,118 = 187
      - East: 5,188 - 5,026 = 162
      - International: 1,951 - 1,404 = 547
    - West has the greatest increase, thus the fastest-growing.

7. **regions_declining_q2_to_q3**:
    - Calculate sales change from Q2 to Q3:
      - West: 4,902 - 4,585 = +317
      - Central: 3,305 - 2,947 = +358
      - East: 5,188 - 5,310 = -122
      - International: 1,951 - 1,622 = +329
    - The only declining region from Q2 to Q3 is East.

8. **international_q3_organic_usd** (subtracting bulk order):
    - Q3 international (including bulk): 1,951
    - Bulk order: 220
    - Organic: 1,951 - 220 = 1,731 (thousands of usd)
    - Converting to usd: 1,731,000

9. **west_excluding_mountain_q3_usd**:
    - West excluding mountain: 4,902 (West total) - 701 (mountain) = 4,201 (thousands of usd)
    - Converting to usd: 4,201,000

### JSON Output:
```json
{
  "q3_total_usd": 15346000,
  "q2_total_usd": 14464000,
  "q2_central_originally_reported_usd": 3047000,
  "q2_to_q3_change_pct": 6.1,
  "top_region_q3": "East",
  "fastest_growing_region_q1_to_q3": "West",
  "regions_declining_q2_to_q3": ["East"],
  "international_q3_organic_usd": 1731000,
  "west_excluding_mountain_q3_usd": 4201000
}
```

This structure includes all the calculated values as derived from the specified sections of the text. Additional notes and restatements have been implemented where necessary.

Size: 15B parameters. First tested OCT 10.

Models that scored about the same

Comments

Sign in with GitHub to comment. Spam and abuse are hidden automatically.