AI의 다음 경쟁은 누가 가장 강한 모델을 만드는가에만 있지 않다. 더 많은 사용자가 더 많은 업무를 더 낮은 비용으로 완료하게 만드는가가 핵심이다.

Astra는 AI가 맡을 수 있는 업무의 상한을 높인다. AWS의 저가 중국·오픈 모델은 지능의 유통 가격을 낮춘다. M5 Ultra 512GB급 머신은 일부 사용자에게 지능을 클라우드에서 임대하는 대신 직접 소유할 선택지를 준다.

세 사례는 서로 다른 뉴스처럼 보이지만, 하나의 산업 변화를 가리킨다.

프런티어 지능은 위로 확장되고, 저가 모델은 옆으로 확산되며, 로컬 머신은 아래에서 독립성을 만든다.

한 줄 요약

AI 비용 경쟁은 토큰당 가격 경쟁에서 성공한 업무 하나를 완료하는 가장 싼 경로를 선택하는 경쟁으로 이동하고 있다.

Uber와 연결되는 성장 루프

지난 Uber 사례의 핵심은 사용자는 늘리고, 단위 비용은 줄인다였다. AI에서도 비슷한 루프가 만들어진다.

추론 비용 하락
  → 더 많은 사용자와 사용량
  → 더 많은 평가·자동화·운영 데이터
  → 라우팅·캐시·하니스·모델 효율 개선
  → 성공한 업무당 비용 하락
  → 다시 사용량 증가

다만 AI에서 늘려야 하는 것은 단순한 호출 수가 아니다. 실제로 완료된 업무와 사용자 가치를 늘려야 한다. 호출량만 늘고 재시도·검토·실패가 함께 늘면 비용 효율이 좋아진 것이 아니다.

세 사례가 맡는 역할

1. Astra — 지능의 상한을 높인다

Astra 관련 발표의 핵심은 AGI라는 명칭보다 다음 질문에 있다.

  • 장시간 자율 작업이 가능한가
  • 수학·과학 연구의 탐색 속도를 높이는가
  • 사이버 능력이 인간 통제 범위를 넘기 시작했는가
  • 모델 자체보다 에이전트 + 도구 + 실행 권한이 핵심 위험 단위가 되는가

Astra는 지능의 상한과 업무 자동화 범위를 보여주는 사례다. 그러나 성능 수치와 AGI 해석은 우선 공급자 발표와 공개 보도로 분류해야 하며, 독립 재현과 실제 업무 성과가 확인되기 전에는 확정적 사실로 쓰지 않는다.

2. AWS의 저가 중국·오픈 모델 — 지능의 유통 가격을 낮춘다

AWS Bedrock과 SageMaker는 Qwen·DeepSeek 계열을 비롯한 오픈웨이트 모델을 관리형 환경에서 제공하고 있다. AWS는 2026년 3월 여러 오픈 모델에 서버리스 파인튜닝도 추가했다. AWS 발표

중요한 점은 “중국 모델이 싸다”에서 끝나지 않는다.

  • 고급 모델의 일정 수준 이상 성능이 널리 공급됨
  • 차별화 기준이 모델명에서 가격·지연시간·통합성으로 이동
  • 기업은 모든 작업에 최고가 모델을 쓸 필요가 없어짐
  • 라우터가 작업 난도와 실패 비용에 따라 모델을 배분할 수 있음

관리형 클라우드에 올라왔다고 해서 데이터 거버넌스, 모델 편향, 공급망, 규제 문제가 자동으로 해결되는 것은 아니다. 제공자, 추론 리전, 데이터 보존, 사고 대응, 대체 모델을 따로 확인해야 한다.

3. M5 Ultra 512GB — 지능의 로컬 소유를 가능하게 한다

Apple은 M5 Ultra Mac Studio에 최대 512GB 통합 메모리와 1.2TB/s 메모리 대역폭을 제공한다고 발표했다. 512GB 구성은 2026년 10월 말 출시 예정으로 안내됐다. Apple 공식 발표

이 장비의 의미는 단순히 “더 빠른 Mac”이 아니다.

  • 민감한 데이터를 외부 API로 보내지 않을 수 있음
  • API 비용과 공급자 종속성을 줄일 수 있음
  • 소규모 팀이 대형 오픈 모델을 직접 실험할 수 있음
  • 반복적인 문서·검색·코드 보조 작업을 고정비로 처리할 수 있음

다만 512GB 메모리가 곧 높은 추론 속도를 의미하지는 않는다. 모델 크기, 양자화, 메모리 대역폭, 컨텍스트 길이, 동시 사용자 수, 소프트웨어 최적화를 함께 봐야 한다. 전력·감가상각·업데이트·운영 비용까지 포함하면 로컬이 항상 클라우드보다 싼 것도 아니다.

세 층으로 분화되는 AI 시장

  1. 프런티어 계층 — Astra급 모델. 높은 성공률과 넓은 자동화 범위를 제공하지만 비용과 접근 통제가 크다.
  2. 클라우드 상품 계층 — 중국·오픈 모델과 관리형 API. 충분한 성능을 낮은 단가와 높은 확장성으로 제공한다.
  3. 로컬 소유 계층 — M5 Ultra급 장비와 온프레미스 모델. 보안·독립성·고정비 예측 가능성을 제공한다.

실제 기업 구성은 하나를 고르는 방식보다 다음과 같은 하이브리드에 가까워질 가능성이 높다.

요청
  ↓
작업 분류·비용·보안 라우터
  ├─ 단순 분류·요약·대량 RAG → 저가 모델
  ├─ 일반 코딩·문서 작업 → 오픈·중간급 모델
  ├─ 복잡한 추론·고위험 판단 → 프런티어 모델
  └─ 민감 정보·반복 업무 → 로컬 모델
       ↓
결과 검증·재시도·감사 로그

핵심 경쟁력은 특정 모델을 고정하는 것이 아니라, 난도·민감도·SLA·실패 비용·예산에 따라 가장 싼 성공 경로를 선택하는 것이다.

측정 단위를 바꿔야 한다

토큰당 가격만 비교하면 중요한 비용을 놓친다. 다음 지표가 더 중요하다.

  • 성공한 업무 1건당 비용
  • 사용자당 유효 업무 완료 수
  • 사람의 수정·검토 시간
  • 실패율과 재시도 횟수
  • 평균·최악 지연시간
  • 데이터가 외부로 나간 비율
  • 모델 교체와 검증에 드는 전환 비용

예를 들어 저가 모델이 한 번에 성공하지 못해 다섯 번 호출된다면, 비싼 모델 한 번보다 총비용이 커질 수 있다. 반대로 프런티어 모델이 높은 성공률로 재작업을 줄이면 토큰 가격이 높아도 업무당 비용은 낮을 수 있다.

판단 기준: 사실과 해석을 분리하기

이 글에서 각 사례는 다음 네 칸으로 관리한다.

  • 사실: 누가 무엇을 발표·판매·제공했는가
  • 근거 강도: 공식 문서, 독립 보도, 커뮤니티 관찰 중 어디에 기반하는가
  • 검증 상태: 독립 재현·실제 업무 검증·학술 검증이 되었는가
  • 해석: 산업 구조에 대해 어떤 의미를 부여하는가

특히 근거가 아직 약함과 거짓임이 확인됨을 구분한다. Astra의 AGI 주장, 수학적 성과, M5 Ultra의 비용 우위는 서로 다른 검증 문제를 가진다.

결론

AI의 다음 승자는 가장 똑똑한 모델을 가진 기업이 아닐 수 있다. 사용자가 더 많은 업무를 맡기고, 시스템이 그 업무를 더 낮은 비용으로 성공시키며, 결과를 사람이 검증할 수 있게 만드는 기업이 유리하다.

Astra는 지능의 상한을 높인다. 저가 클라우드 모델은 지능의 가격을 낮춘다. M5 Ultra 같은 로컬 머신은 지능의 소유권을 넓힌다.

따라서 이 글의 최종 질문은 “AGI가 도착했는가?”가 아니다.

누가 지능을 가장 싸고, 안전하고, 검증 가능한 업무 성과로 바꾸는가?

참고 자료

Data Science를 학습하면서, 전처리에 많이 사용되는 groupby를 정리해본다. 예제 데이터는 아래를 가정한다.

import pandas as pd

df  # columns: SEASON, A, B, C  (SEASON은 반복되는 범주값)

1. 개요 — Series가 모여 DataFrame이 된다

1.1 Series = 값 배열 + 인덱스

Series는 1차원이고, 값과 라벨(인덱스)이 짝지어진 구조다.

s = pd.Series([10, 20, 30], index=['x', 'y', 'z'], name='A')
x    10
y    20
z    30
Name: A, dtype: int64
  • s.values → array([10, 20, 30])
  • s.index → Index(['x','y','z'])
  • s.name → 'A' (나중에 DataFrame의 열 이름이 된다)

1.2 DataFrame = 인덱스를 공유하는 Series들의 모음

df = pd.DataFrame({
    'A': pd.Series([10, 20, 30], index=['x','y','z']),
    'B': pd.Series([1.0, 2.0, 3.0], index=['x','y','z']),
})
     A    B
x   10  1.0
y   20  2.0
z   30  3.0

핵심은 축이 두 개라는 점이다.

구성요소 의미
df.index 행 라벨 (axis=0)
df.columns 열 라벨 (axis=1)
df['A'] 열 하나 → Series
df[['A','B']] 열 여러 개 → DataFrame
df.loc['x'] 행 하나 → Series (인덱스가 열 이름이 된다)

대괄호 하나(df['A'])와 둘(df[['A']])의 차이가 이후 전부에 영향을 준다. 하나는 Series, 둘은 DataFrame이다.

1.3 MultiIndex = 라벨이 여러 층

인덱스의 한 칸이 단일 값이 아니라 튜플인 경우다.

mi = pd.MultiIndex.from_tuples([('2022','A'), ('2022','B'), ('2023','A')],
                               names=['SEASON', 'VAR'])
SEASON  VAR
2022    A
        B
2023    A
  • mi.nlevels → 2
  • level=0 → 바깥층(SEASON), level=1 → 안쪽층(VAR)
  • 이름이 있으면 숫자 대신 이름 사용 가능: level='SEASON'

groupby의 결과나 corr(), describe(), pivot 계열은 MultiIndex를 자주 만들어낸다. 그래서 "층을 어떻게 골라내고 없애느냐"가 실무의 대부분이다.


2. groupby 개요

2.1 split - apply - combine

df.groupby('SEASON')['A'].mean()
  1. split — SEASON 값이 같은 행끼리 묶는다
  2. apply — 각 묶음에 mean()을 적용한다
  3. combine — 결과를 다시 하나로 합친다. 그룹 키가 결과의 인덱스가 된다
SEASON
2022    18.3
2023    21.7
Name: A, dtype: float64

2.2 groupby 객체 자체는 계산을 하지 않는다

g = df.groupby('SEASON')
g                       # <DataFrameGroupBy object ...>  ← 아직 아무것도 안 함
g.ngroups               # 그룹 개수
g.size()                # 그룹별 행 수
g.groups.keys()         # 그룹 키 목록
g.get_group('2022')     # 특정 그룹의 원본 행들
list(g)[0]              # ('2022', DataFrame) 튜플

집계 함수(mean, agg, sum 등)를 붙이는 순간 실제로 계산된다. 디버깅할 때 get_group으로 한 그룹만 꺼내 확인하는 습관이 유용하다.

2.3 반환 형태 규칙

df.groupby('SEASON')['A'].mean()          # Series      (열 하나 → 1차원)
df.groupby('SEASON')[['A']].mean()        # DataFrame   (대괄호 둘)
df.groupby('SEASON')[['A','B']].mean()    # DataFrame
df.groupby(['SEASON','TEAM'])['A'].mean() # Series, 인덱스는 MultiIndex

2.4 자주 쓰는 옵션

df.groupby('SEASON', as_index=False)   # 그룹 키를 인덱스가 아닌 일반 열로
df.groupby('SEASON', sort=False)       # 그룹 키 정렬 안 함 (등장 순서 유지, 약간 빠름)
df.groupby('SEASON', dropna=False)     # 키가 NaN인 행도 하나의 그룹으로
df.groupby('SEASON', observed=True)    # Categorical 키에서 실제 등장한 조합만

as_index=False를 안 썼다면 나중에 .reset_index()로 같은 효과를 낼 수 있다.

2.5 agg / transform / apply 구분

셋의 차이를 헷갈리면 이후가 전부 꼬인다.

메서드 그룹당 반환 결과 행 수 용도
agg 스칼라 1개 그룹 수 평균, 합, max-min
transform 원본과 같은 길이 원본 행 수 그룹평균 붙이기, 정규화
apply 자유 (스칼라/Series/DataFrame) 상황에 따라 복잡한 그룹 로직
# agg: 시즌별 평균 (행 수 = 시즌 수)
df.groupby('SEASON')['A'].mean()

# transform: 각 행에 자기 시즌 평균을 붙인다 (행 수 = 원본)
df['A_season_mean'] = df.groupby('SEASON')['A'].transform('mean')
df['A_z'] = df.groupby('SEASON')['A'].transform(lambda s: (s - s.mean()) / s.std())

# apply: 여러 열을 동시에 봐야 할 때
df.groupby('SEASON').apply(
    lambda g: g['A'].corr(g['B']),
    include_groups=False,      # pandas 2.2+ 경고 방지
)

apply는 유연하지만 느리다. agg 또는 transform으로 표현 가능하면 그쪽을 쓴다.


3. groupby + corr 결과 인덱싱

3.0 만들어지는 구조 확인

c = df.groupby('SEASON')[['A', 'B', 'C']].corr()

문법 주의: df.groupby('SEASON').['A','B','C'] 는 오류. 대괄호를 두 번 쓴다 → [['A','B','C']]

                   A      B      C
SEASON  VAR
2022    A       1.00   0.35  -0.12
        B       0.35   1.00   0.48
        C      -0.12   0.48   1.00
2023    A       1.00   0.71   0.05
        B       0.71   1.00   0.22
        C       0.05   0.22   1.00
2024    A       1.00   0.19   0.60
        B       0.19   1.00  -0.33
        C       0.60  -0.33   1.00
  • 행 인덱스: (SEASON, VAR) 2층
  • 열: A, B, C
  • 시즌마다 3×3 대칭행렬이 세로로 쌓인 모양
  • 대각선은 항상 1.0, 상삼각과 하삼각은 중복. 실제 정보는 쌍 3개(A-B, A-C, B-C)뿐이다
c.index.names    # ['SEASON', None]
c.index[0]       # ('2022', 'A')

3.1 인덱스로 직접 접근 (loc)

스칼라 하나

c.loc[('2022', 'A'), 'B']      # 2022의 corr(A, B) → 0.35

행 라벨이 튜플이므로 ('2022','A')를 통째로 넘긴다.

한 시즌의 행렬 전체

c.loc['2022']
        A      B      C
VAR
A    1.00   0.35  -0.12
B    0.35   1.00   0.48
C   -0.12   0.48   1.00

바깥층은 전부, 안쪽층은 특정 값 — slice(None) 사용

c.loc[(slice(None), 'A'), :]     # 모든 시즌의 A행
c.loc[(slice(None), 'A'), 'B']   # 모든 시즌의 corr(A,B)

slice(None)이 눈에 거슬리면 IndexSlice가 같은 뜻이다.

idx = pd.IndexSlice
c.loc[idx[:, 'A'], 'B']
c.loc[idx['2022':'2023', 'A'], :]    # 시즌 범위 + 안쪽층 지정

이 방식의 결과는 인덱스가 여전히 2층이다.

c.loc[idx[:, 'A'], 'B'].idxmax()     # ('2023', 'A')  ← 튜플이 나온다

시즌 라벨만 원하면 [0]을 떼거나, 아래 xs를 쓴다.

MultiIndex 정렬 경고 loc에 슬라이스를 쓰다가 UnsortedIndexError가 나면 먼저 정렬한다.

c = c.sort_index()

3.2 xs로 단면 자르기

xs = cross-section. 지정한 층의 라벨이 일치하는 행만 골라내고, 그 층을 인덱스에서 제거한다.

c.xs('A', level=1)
          A      B      C
SEASON
2022   1.00   0.35  -0.12
2023   1.00   0.71   0.05
2024   1.00   0.19   0.60

동작을 두 단계로 풀면:

  1. VAR == 'A' 인 행만 남긴다 → 시즌당 1행
  2. VAR 층은 전부 'A'라 정보가 없으므로 인덱스에서 떨어뜨린다

읽는 법: "각 시즌에서 A가 다른 변수와 갖는 상관계수". A 열은 자기 자신이므로 항상 1.00이고, 쓸모 있는 건 B, C 열이다.

인덱스가 1층이 됐으므로 이후는 평범한 DataFrame이다.

c.xs('A', level=1)['B']
SEASON
2022    0.35
2023    0.71
2024    0.19
Name: B, dtype: float64

시즌별 corr(A,B) Series. idxmax, max, sort_values, plot 전부 그대로 동작한다.

c.xs('A', level=1)['B'].idxmax()    # '2023'  ← 튜플이 아니라 라벨

level을 생략하면 뜻이 달라진다

c.xs('A', level=1)   # 안쪽층(VAR)에서 'A' 찾기       ← 의도한 것
c.xs('2022')         # 바깥층(level=0 기본)에서 찾기   → 한 시즌 행렬
c.xs('A')            # 바깥층에서 'A'를 찾다가 KeyError

기본값이 level=0이므로 안쪽층을 노릴 때 level=1은 필수다.

층을 남기고 싶으면

c.xs('A', level=1, drop_level=False)

대칭성 상관행렬은 대칭이므로 어느 방향으로 뽑아도 값이 같다.

c.xs('A', level=1)['B']    # A행의 B열
c.xs('B', level=1)['A']    # B행의 A열 → 동일한 값

실전 조회 패턴

ab = c.xs('A', level=1)['B']   # 시즌별 corr(A, B)
ac = c.xs('A', level=1)['C']   # 시즌별 corr(A, C)
bc = c.xs('B', level=1)['C']   # 시즌별 corr(B, C)

s_best = ab.idxmax()           # corr(A,B) 최대 시즌
ab.max()                       # 그때의 corr(A,B) 값
ac.loc[s_best]                 # 그 시즌의 corr(A,C)
bc.loc[s_best]                 # 그 시즌의 corr(B,C)

bc.loc[ac.idxmax()]            # corr(A,C) 최대 시즌의 corr(B,C)
ab.nlargest(3)                 # corr(A,B) 상위 3시즌

3.3 unstack으로 평탄화 (flat)

무슨 일이 일어나는가

unstack()은 행 인덱스의 가장 안쪽 층을 뽑아 열로 올려붙인다. 시즌마다 3행이었던 것이 시즌마다 1행이 된다.

flat = c.unstack()
            A                     B                     C
VAR         A      B      C       A      B      C       A      B      C
SEASON
2022     1.00   0.35  -0.12    0.35   1.00   0.48   -0.12   0.48   1.00
2023     1.00   0.71   0.05    0.71   1.00   0.22    0.05   0.22   1.00
2024     1.00   0.19   0.60    0.19   1.00  -0.33    0.60  -0.33   1.00
  • 행 인덱스: SEASON 1층으로 줄었다
  • 열 인덱스: 2층으로 늘었다 (3 × 3 = 9개 열)
  • 층의 총 개수는 보존된다. 행에서 뺀 만큼 열로 옮겨갔을 뿐이다

열 튜플 읽는 규칙 (가장 헷갈리는 지점)

flat.columns
# MultiIndex([('A','A'), ('A','B'), ('A','C'),
#             ('B','A'), ('B','B'), ('B','C'),
#             ('C','A'), ('C','B'), ('C','C')])

(원래_열이름, 올라온_VAR값) — 기존 열이 겉층, 새로 올라온 층이 안층.

따라서 ('B','A') = "원래 표에서 VAR='A' 행의 B 열" = corr(A, B).

flat[('B', 'A')]     # 시즌별 corr(A, B)
flat['B']['A']       # 같은 것, 두 단계 접근
flat['B']            # B 겉층 전체 묶음

사람이 읽기 좋게 이름 붙이기

flat.columns = [f'{b}_{a}' for a, b in flat.columns]

a = 겉층(원래 열), b = 안층(VAR). 순서를 뒤집어 ('B','A') → 'A_B' 로 만든다.

A_A  B_A  C_A   A_B  B_B  C_B   A_C  B_C  C_C
 ↑                ↑                     ↑
1.0            corr(A,B)            corr(B,C)

대각선(A_A, B_B, C_C)은 1.0이고 B_A는 A_B의 중복이므로, 의미 있는 3개만 남긴다.

flat = flat[['A_B', 'A_C', 'B_C']]
        A_B    A_C    B_C
SEASON
2022   0.35  -0.12   0.48
2023   0.71   0.05   0.22
2024   0.19   0.60  -0.33

시즌이 행, 상관쌍이 열인 평범한 표가 됐다. 조회가 한 줄로 끝난다.

flat.loc[flat['A_B'].idxmax(), 'A_C']    # corr(A,B) 최대 시즌의 corr(A,C)
flat.loc[flat['A_C'].idxmax(), 'A_B']
flat.nlargest(3, 'A_B')                   # 상위 3시즌 전체 비교
flat.sort_values('B_C', ascending=False)
flat.corr()                               # 쌍끼리의 관계
flat.plot()                               # 시즌 추이 그래프

unstack(level=0) — 방향 반대

c.unstack(level=0)      # == c.unstack('SEASON')
          A                    B                 ...
SEASON  2022   2023   2024   2022   2023   2024
VAR
A       1.00   1.00   1.00   0.35   0.71   0.19
B       0.35   0.71   0.19   1.00   1.00   1.00
C      -0.12   0.05   0.60   0.48   0.22  -0.33

변수가 행, 시즌이 열. 시즌 수가 많으면 열이 폭발하므로 보통 기본값(VAR을 올리는 쪽)이 실용적이다.

역연산 stack

flat.stack()     # 열 층을 행으로 내린다 → 원래 c 모양에 가깝게 복귀

3.4 행렬을 거치지 않는 대안

corr()은 대각선과 중복을 모두 계산한다. 쌍 값만 필요하면 처음부터 직접 만드는 게 가볍고 명확하다.

pairs = df.groupby('SEASON').apply(
    lambda g: pd.Series({
        'A_B': g['A'].corr(g['B']),
        'A_C': g['A'].corr(g['C']),
        'B_C': g['B'].corr(g['C']),
    }),
    include_groups=False,
)
pairs.loc[pairs['A_B'].idxmax()]

또는 reset_index로 아예 긴 형태(long format)로 펴는 방법.

t = c.reset_index().rename(columns={'level_1': 'VAR'})
t.loc[t['VAR'] == 'A', ['SEASON', 'B', 'C']]

3.5 세 방식 비교

방식 결과 인덱스 idxmax() 반환 언제
loc / IndexSlice 2층 유지 튜플 ('2023','A') 스칼라 하나, 범위 슬라이싱
xs(..., level=1) 1층으로 축소 라벨 '2023' 특정 변수 기준 조회 (가장 흔함)
unstack + 이름정리 1층 + 열 평탄 라벨 '2023' 여러 쌍을 나란히 비교/정렬/시각화

4. groupby().agg() 사용법

4.1 named aggregation — 새컬럼명=(원본컬럼, 함수)

권장 기본형이다. 결과 열이 평평한 단일 레벨로 나와 후처리가 없다.

out = df.groupby('SEASON').agg(
    A_mean=('A', 'mean'),
    B_range=('B', lambda s: s.max() - s.min()),
)
        A_mean  B_range
SEASON
2022      18.3      7.2
2023      21.7      5.4

lambda 대신 이름 있는 함수를 쓰면 재사용과 가독성이 좋다.

def rng(s):
    return s.max() - s.min()

df.groupby('SEASON').agg(A_mean=('A', 'mean'), B_range=('B', rng))

한 열에 여러 집계, 서로 다른 열을 자유롭게 섞을 수 있다.

df.groupby('SEASON').agg(
    A_mean=('A', 'mean'),
    A_std=('A', 'std'),
    B_max=('B', 'max'),
    B_min=('B', 'min'),
    B_range=('B', rng),
    C_median=('C', 'median'),
    N=('A', 'size'),                       # 행 수 (NaN 포함)
    N_valid=('A', 'count'),                # 결측 제외 개수
    A_p90=('A', lambda s: s.quantile(0.9)),
)

제약: 키워드 인자 이름이므로 파이썬 식별자여야 한다. SALES 2023=(...) 같은 건 불가.

4.2 dict 방식 — agg({열: 함수})

df.groupby('SEASON').agg({'A': 'mean', 'B': rng})              # 단일 → 열이 평평
df.groupby('SEASON').agg({'A': ['mean', 'std'], 'B': [rng]})   # 다중 → 열이 MultiIndex

다중일 때 결과:

            A               B
         mean    std      rng
SEASON
2022     18.3    3.1      7.2

평탄화는 습관적으로 붙여두면 편하다.

g = df.groupby('SEASON').agg({'A': ['mean','std'], 'B': [rng]})
g.columns = ['_'.join(col) for col in g.columns]    # A_mean, A_std, B_rng
g = g.reset_index()

dict가 유리한 경우

1) 열이 많고 집계가 반복적일 때

num_cols = df.select_dtypes('number').columns
df.groupby('SEASON').agg({c: 'mean' for c in num_cols})

named agg로는 **{f'{c}_mean': (c,'mean') for c in num_cols} 언패킹이 필요해 장황하다.

2) 집계 스펙을 외부(설정/인자)에서 주입할 때

spec = {'A': 'mean', 'B': rng, 'C': ['min', 'max']}
df.groupby('SEASON').agg(spec)

3) MultiIndex 열을 그대로 활용할 때

g = df.groupby('SEASON').agg({'A': ['mean','std'], 'B': ['mean','std']})
g.stack(level=0)      # 변수 × 통계량 형태로 재배치
g['A']['mean']        # 계층 접근

named agg는 항상 1층이라 이런 재배치가 안 된다.

4) 열 이름이 파이썬 식별자가 아닐 때 — 'SALES 2023', '매출/원' 등

5) pivot_table의 aggfunc — named agg 문법 자체가 없다

df.pivot_table(index='SEASON', aggfunc={'A': 'mean', 'B': rng})

주의: dict 안에서 이름 바꾸기는 제거됐다

pandas 0.20 deprecated → 2.0 제거. 아래는 모두 에러다.

df.groupby('SEASON').agg({'A': {'평균': 'mean'}})     # ✗
df.groupby('SEASON')['A'].agg({'평균': 'mean'})       # ✗

이름을 정하려면 named agg를 쓰거나, dict로 집계한 뒤 rename한다.

4.3 Series에 대한 agg

열 하나만 다룰 때는 리스트로 넘기면 된다.

df.groupby('SEASON')['A'].agg(['mean', 'std', 'max', rng])
df.groupby('SEASON')['A'].agg(평균='mean', 범위=rng)    # Series named agg (가능)

4.4 자주 쓰는 집계 문자열

'mean' 'median' 'sum' 'min' 'max' 'std' 'var' 'count' 'size' 'first' 'last' 'nunique' 'idxmax' 'idxmin' 'any' 'all' 'sem' 'skew' 'prod'

문자열은 내부 최적화 경로를 타서 lambda보다 훨씬 빠르다. 가능하면 문자열을 쓴다.

  • count vs size: count는 NaN 제외, size는 포함
  • idxmax: 그룹 내 최대값의 인덱스 라벨을 돌려준다

4.5 두 방식 선택 기준

상황 선택
열 몇 개, 결과 이름을 명시하고 싶다 named agg
열을 동적으로 생성 / 스펙 주입 dict
MultiIndex 열로 후처리 (stack 등) dict
열 이름이 파이썬 식별자가 아니다 dict
pivot_table dict

한 agg 호출 안에서 두 방식을 섞을 수는 없다. 하나만 고른다.


5. 자주 밟는 함정

증상 원인 / 해결
df.groupby('S').['A','B'] 문법 오류 [['A','B']] 대괄호 두 번
xs('A')에서 KeyError level 기본값이 0. 안쪽층은 level=1 명시
idxmax()가 튜플을 반환 loc/IndexSlice는 층을 남긴다. xs나 unstack을 쓰거나 [0]으로 꺼낸다
UnsortedIndexError c = c.sort_index() 후 슬라이싱
unstack 후 열 접근 실패 튜플 순서는 (원래열, 올라온층). 이름 붙일 때 뒤집는다
agg 결과 열이 2층 dict + 다중 함수. '_'.join으로 평탄화
agg({'A': {'평균':'mean'}}) 에러 제거된 문법. named agg 사용
apply에서 그룹 키 관련 FutureWarning include_groups=False 추가 (pandas 2.2+)
np.ptp 사용 시 경고 직접 lambda s: s.max()-s.min() 정의
결과 행 수가 예상과 다름 agg(그룹 수) vs transform(원본 수) 혼동
그룹 키가 인덱스로 들어가 불편 as_index=False 또는 .reset_index()
키가 NaN인 행이 사라짐 기본 dropna=True. dropna=False

6. 한눈 치트시트

# 구조 확인
c.index.names / c.index.nlevels / c.columns

# 기본 집계
df.groupby('SEASON')['A'].mean()                       # Series
df.groupby('SEASON')[['A','B']].mean()                 # DataFrame
df.groupby('SEASON', as_index=False)['A'].mean()       # 키를 열로

# corr 결과 인덱싱
c = df.groupby('SEASON')[['A','B','C']].corr()
c.loc[('2022','A'), 'B']                               # 스칼라
c.loc['2022']                                          # 한 시즌 행렬
c.xs('A', level=1)                                     # A행만, 층 제거
c.xs('A', level=1)['B']                                # 시즌별 corr(A,B)
flat = c.unstack()
flat.columns = [f'{b}_{a}' for a, b in flat.columns]
flat = flat[['A_B','A_C','B_C']]

# 조회
ab, ac = flat['A_B'], flat['A_C']
ac.loc[ab.idxmax()]                                    # A_B 최대 시즌의 A_C
flat.nlargest(3, 'A_B')

# agg
df.groupby('SEASON').agg(A_mean=('A','mean'), B_range=('B', rng))
df.groupby('SEASON').agg({'A': ['mean','std'], 'B': [rng]})

# transform (원본 행 수 유지)
df['A_dev'] = df['A'] - df.groupby('SEASON')['A'].transform('mean')

핵심 주장

기업의 AI 운영에는 두 극단이 있다. 하나는 100x 엔지니어를 지향하며 가능한 한 많은 모델 호출과 에이전트를 사용하게 하는 접근이다. 다른 하나는 비용을 최소화하기 위해 사용량을 강하게 제한하는 접근이다.

실용적인 해법은 둘 중 하나를 택하는 것이 아니다. 사용자와 업무 채택은 넓히되, 문맥·모델·호출·재시도·캐시를 시스템적으로 최적화하고, 최종 평가는 토큰이 아니라 완료된 업무의 가치로 하는 것이다.

Uber의 최근 변화는 이 중간 지점을 보여주는 사례다.

1. 문제 제기: AI를 많이 쓰는 것이 생산성인가

Uber는 2026년 초 개발자들의 Claude Code 등 에이전트 사용을 빠르게 확대했다. 그 결과 연간 AI 코딩 예산을 1분기 말부터 약 4개월 안에 소진했다는 보도가 나왔다.

중요한 점은 AI 도입이 실패해서 비용이 발생한 것이 아니라는 점이다. 사용자가 빠르게 늘고, 에이전트가 여러 작업을 병렬로 수행하면서 사용량이 예상을 넘어섰다. 다시 말해 성공적인 채택이 비용 문제를 일으킨 사례에 가깝다.

그러나 높은 토큰 사용량이 곧 고객 가치나 제품 출시 증가를 의미하지는 않는다. 따라서 질문은 “AI를 얼마나 많이 사용하는가?”에서 다음으로 바뀌어야 한다.

AI가 실제로 완료한 업무 하나의 비용은 얼마인가?

2. 최근 트렌드: 무제한 사용과 무조건 제한 사이

극단 1: 100x 엔지니어와 token-maxxing

고성능 모델과 다중 에이전트를 최대한 사용하면 개인의 처리량을 크게 높일 수 있다. 탐색, 프로토타이핑, 대규모 마이그레이션처럼 실패 비용이 낮거나 속도가 중요한 업무에는 이 접근이 합리적일 수 있다.

하지만 다음 문제가 생긴다.

  • 에이전트가 같은 문맥을 반복해서 읽음
  • 단순 업무에도 프런티어 모델을 사용함
  • 병렬 에이전트가 중복 결과를 만듦
  • 긴 추론과 재시도가 비용을 키움
  • AI가 만든 코드·테스트·리뷰를 사람이 다시 검토해야 함

극단 2: 사용량 제한 중심의 비용 통제

사용자별 월 한도나 고정 예산은 급격한 비용 폭증을 막는 데 유용하다. 하지만 한도 자체가 목표가 되면 중요한 작업까지 막고, 비용은 줄어도 생산성·학습·실험 기회를 잃을 수 있다.

현실적인 중간 지점

실용적인 운영은 채택은 확대하고, 낭비만 줄이는 것이다.

  • 사용자는 자유롭게 AI를 활용
  • 시스템은 업무별로 적절한 모델을 선택
  • 문맥과 도구 호출을 최소화
  • 세션 비용을 사용자에게 공개
  • 결과 품질과 완료 업무를 함께 측정

3. Uber의 비용 방정식

Uber는 에이전트 세션 비용을 다음처럼 분해한다.

사용자 수 × 사용자당 세션 수 × 세션당 턴 수 × 턴당 요청 수 × 요청당 토큰 수 × 토큰 가격

이 식의 해석은 중요하다.

확대해야 하는 항목

  • 사용자 수
  • 사용자당 유효 세션 수

AI가 조직에 실제로 확산되려면 이 두 항목은 반드시 커져야 한다. Uber도 2026년 2월부터 8월 사이 주간 활성 사용자가 7배, 주간 에이전트 요청이 9.4배 증가했다고 설명한다.

최적화해야 하는 항목

  • 세션당 턴 수
  • 턴당 요청 수
  • 요청당 입력·출력 토큰
  • 토큰 가격

이 항목들은 사용자의 AI 접근을 막지 않고도 줄일 수 있다. Uber는 같은 모델을 고정해 비교했을 때 요청 1,000건당 비용을 약 34%, 세션당 비용을 약 52% 낮췄다고 밝혔다.

4. Uber가 최적화한 것

4.1 불필요한 대화 재전송

에이전트는 매 턴마다 대화 이력, 프로젝트 컨텍스트, 도구 결과를 다시 입력으로 보낼 수 있다. 세션이 길어질수록 같은 정보가 반복 청구된다.

Uber는 자동 압축 기준을 40만 토큰으로 설정하고, 100만 토큰 컨텍스트를 지원하는 모델이라도 무조건 끝까지 문맥을 유지하지 않는다. 긴 문맥이 항상 높은 품질을 보장하지 않는다는 판단이다.

4.2 MCP 도구와 컨텍스트 최적화

Uber는 1,000개 이상의 MCP 서버를 단일 게이트웨이로 관리한다. 모든 도구의 스키마를 매 세션에 미리 넣으면 초기 프롬프트에 약 5만~7만 토큰이 추가될 수 있다.

대응 방식은 다음과 같다.

  • 필요한 도구만 검색해 동적으로 로딩
  • MCP 호출을 CLI 방식으로 지연 실행
  • 여러 도구 호출을 코드 모드에서 묶어 처리
  • 중간 polling 결과를 모델 문맥에 계속 넣지 않음

핵심은 MCP 서버를 줄이는 것이 아니라 모든 MCP 서버의 설명을 모든 세션에 싣지 않는 것이다.

4.3 프롬프트 캐시의 운영

반복되는 긴 문맥은 캐시하면 입력 비용과 지연 시간을 줄일 수 있다. Uber는 개발자가 세션을 5분 이상 중단하는 경우가 많다는 관찰에 따라 대화형 세션의 캐시 유지 시간을 5분에서 1시간으로 바꿨다.

반면 짧게 끝나는 하위 에이전트에는 짧은 캐시 시간을 적용한다. 즉, 캐시도 일괄 설정이 아니라 세션의 수명과 작업 패턴에 맞춰 운영한다.

4.4 적절한 모델 배치

모든 작업에 가장 비싼 모델을 쓰지 않는다.

  • 계획 수립·복잡한 판단: 고성능 모델
  • 분류·검색·반복 실행: 저비용 모델
  • 특화 업무: 오픈웨이트 모델
  • 하위 에이전트: 주 모델보다 저렴한 모델

Uber는 실제 업무로 자체 벤치마크를 만들고, 모델별 비용·정확도·신뢰성의 Pareto frontier를 비교한다. 단순히 “가장 싼 모델”이 아니라 완료된 업무의 비용과 품질이 함께 좋은 모델을 고르는 방식이다.

4.5 비용을 사용자에게 보이기

개발자 터미널에 세션 비용을 표시하고, 대시보드가 다음과 같은 낭비 패턴을 알려준다.

  • 단순 업무에 고성능 모델 사용
  • 긴 MCP 응답을 계속 문맥에 유지
  • 캐시 만료 후 세션 재개
  • 과도한 시스템 지침과 도구 정의 사전 로딩

비용 관리는 재무팀만의 업무가 아니라, 개발자의 일상적인 설계 선택이 된다.

5. 다른 사례: 비용 최적화가 제품 기능으로 들어가는 중

Uber만의 특수한 대응은 아니다.

GitHub Copilot: 하니스가 모델만큼 중요해짐

GitHub는 같은 모델과 같은 작업을 고정해 비교했을 때, Copilot의 에이전트 하니스가 다른 하니스보다 적은 토큰으로 비슷한 작업 완료율을 달성한다고 발표했다. 이는 비용 경쟁의 단위가 모델 자체에서 모델을 운용하는 하니스와 오케스트레이션 계층으로 확장되고 있다는 신호다. GitHub 공식 글

VS Code: 캐시 적중률을 핵심 효율 지표로 관리

VS Code 팀도 반복되는 프롬프트 접두부를 재사용하면 비용과 지연 시간을 함께 줄일 수 있다고 설명한다. 대화형 코딩 도구의 성능은 모델 품질뿐 아니라 컨텍스트를 얼마나 잘 재사용하는지에 좌우된다. VS Code 공식 글

Snowflake: 동적 모델 라우팅을 게이트웨이화

Snowflake는 품질과 비용을 기준으로 작업별 모델을 자동 선택하는 Cortex AI Gateway를 내세우고 있다. 복잡한 작업은 프런티어 모델로, 반복·저난도 작업은 더 효율적인 모델로 보내는 방식이다. Snowflake의 내부 평가에서는 프런티어 모델 단일 경로보다 최대 3배 높은 토큰 효율을 보고했다. 다만 이는 Snowflake 내부 테스트이므로 일반적인 ROI로 확대 해석해서는 안 된다. 이는 기업의 모델 선택이 개인의 프롬프트 습관이 아니라 중앙화된 라우팅 정책으로 이동하는 사례다. Snowflake 공식 글

이 사례들은 Uber의 방향과 같은 축에 있다. 사용량을 단순히 억제하는 것이 아니라, 하니스·캐시·게이트웨이·모델 라우팅을 통해 같은 업무를 더 적은 비용으로 수행한다.

6. 살펴봐야 할 부분: 비용 효율이 ROI인가

Uber의 요청당 비용과 세션당 비용이 낮아졌다는 사실은 좋은 신호지만, 그것만으로 사업 ROI가 증명되지는 않는다.

반드시 함께 봐야 할 지표는 다음과 같다.

  • 병합된 PR당 AI 비용
  • 실제 출시된 기능당 AI 비용
  • AI 생성 코드의 되돌림률과 장애율
  • 사람의 검토·수정 시간
  • 버그 해결 시간과 MTTR
  • 고객 지원·매출·운영비에 미친 영향
  • AI 사용이 없었을 때와 비교한 순수한 시간 절감

특히 비용/요청은 좋아졌지만 비용/고객 가치는 그대로일 수 있다. 요청 수가 늘면서 단위 비용이 내려가는 규모의 경제와, 실제 가치가 증가하는 생산성 향상은 구분해야 한다.

7. 우리가 배울 수 있는 운영 원칙

  1. AI 사용을 무조건 제한하지 말고, 먼저 비용의 발생 구조를 분해한다.
  2. 모델 가격보다 문맥 중복·재시도·도구 로딩·캐시 만료를 점검한다.
  3. 단순 작업과 복잡한 작업에 같은 모델을 배치하지 않는다.
  4. 실제 업무에서 만든 평가 세트로 비용과 품질을 함께 비교한다.
  5. 사용자·세션 비용을 숨기지 않고 실시간으로 보여준다.
  6. 토큰당 비용에서 완료된 업무당 비용으로 측정 단위를 바꾼다.
  7. 비용 절감 결과가 고객 가치와 연결되는지 별도 검증한다.

결론

AI 운영의 목표는 최대 사용량도, 최소 비용도 아니다. 조직이 필요한 만큼 충분히 사용하면서, 낭비되는 토큰과 낮은 가치의 호출을 줄이고, 결과 단위의 성과를 높이는 것이다.

Uber 사례가 보여주는 변화는 다음과 같다.

1단계: AI를 조직에 확산한다.
2단계: 사용량의 비용 구조를 이해한다.
3단계: 업무 단위의 품질과 ROI를 최적화한다.

 

따라서 앞으로의 AI 경쟁력은 “누가 가장 강한 모델을 쓰는가”보다 누가 같은 모델 호출로 더 많은 유효 업무를 완료하는가에 가까워질 가능성이 높다.

참고 자료 및 근거 상태

작성 메모

초기 보도에서 Uber의 연간 AI 예산 소진 시점은 “1분기”와 “약 4개월”로 혼용된다. 본문에서는 단정 대신 “1분기 말부터 약 4개월 내”로 표현하는 것이 안전하다.

+ Recent posts