Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

보강 4-3 실습 — 함정을 하나씩 재현한다

서술의 열넷은 전부 계산은 맞는데 결론이 틀린 자리였다. 이 노트북은 그 함정을 코드로 다시 놓아 본다.

  1. 검산 — 열넷의 답을 다시 잰다.

  2. 함정 재현 — 대각합이 같은데 갈리는 것, 선행 주소행렬식이 놓치는 것, 최소제곱이 강제하는 직교, 유사변환이 옮기는 것.

  3. 회수 지도 — 예순 문제가 서른일곱 강의를 어떻게 되짚었는지 원고에서 읽어 센다.

import itertools

import numpy as np

np.set_printoptions(precision=4, suppress=True, linewidth=115)


def 같다(번호, 얻은, 적힌, 말="", 허용=1e-9):
    얻은, 적힌 = np.asarray(얻은, dtype=float), np.asarray(적힌, dtype=float)
    차 = np.abs(얻은 - 적힌).max()
    print(f"[{번호}] {'맞다  ' if 차 < 허용 else '틀리다'} "
          f"차이 {차:.2e}   {말}")

1. 관측표는 대칭이 아니다 (문제 47)

이론이 대칭이라고 말한 것은 보상 반응표이지 관측표가 아니었다.

S = np.ones((3, 3)) - 3 * np.eye(3)
x = np.array([2.0, 2, 2])
dm = np.array([0.5, 0.25, 0.25])
D = S - np.outer(dm, x)

같다("47", S, S.T, "이론이 말하는 표는 대칭")
같다("47", np.sort(np.linalg.eigvalsh(S)), [-3, -3, 0], "고윳값 0, -3, -3")
같다("47", S @ np.ones(3), np.zeros(3), "S p = 0 (값이 같이 오르면 안 바뀐다)")
print(f"     관측표 D =\n{D}")
print(f"     D 는 대칭인가 : {np.allclose(D, D.T)}   D12={D[0,1]}, D21={D[1,0]}")
print(f"     D 의 행 합 {D.sum(axis=1)}  -> D p 도 0 이 아니다")

# 어긋남이 잡음이라면 규칙적일 리 없다
print()
print(f"{'(i,j)':>8} {'D_ij - D_ji':>12} {'(dm_j - dm_i) x':>18}")
for i, j in ((0, 1), (0, 2), (1, 2)):
    왼 = D[i, j] - D[j, i]
    오 = (dm[j] - dm[i]) * x[i]
    print(f"{f'({i+1},{j+1})':>8} {왼:>12.2f} {오:>18.2f}")
print("  -> 잡음이 아니라 계산 가능한 양이다")
[47] 맞다   차이 0.00e+00   이론이 말하는 표는 대칭
[47] 맞다   차이 1.11e-16   고윳값 0, -3, -3
[47] 맞다   차이 0.00e+00   S p = 0 (값이 같이 오르면 안 바뀐다)
     관측표 D =
[[-3.   0.   0. ]
 [ 0.5 -2.5  0.5]
 [ 0.5  0.5 -2.5]]
     D 는 대칭인가 : False   D12=0.0, D21=0.5
     D 의 행 합 [-3.  -1.5 -1.5]  -> D p 도 0 이 아니다

   (i,j)  D_ij - D_ji    (dm_j - dm_i) x
   (1,2)        -0.50              -0.50
   (1,3)        -0.50              -0.50
   (2,3)         0.00               0.00
  -> 잡음이 아니라 계산 가능한 양이다

자료를 더 모아도 DD 는 대칭이 안 된다. 표본이 무한해도 그렇다. 빼야 할 항을 안 뺐기 때문이다.

# 표본을 늘려 보면 정말 안 가까워지는지
난수 = np.random.default_rng(40)
print(f"{'표본 수':>10} {'|D - D^T| 최대':>16}")
for n in (50, 500, 5000, 50000):
    잡음 = 난수.normal(scale=1 / np.sqrt(n), size=(3, 3))
    관측 = D + 잡음
    print(f"{n:>10} {np.abs(관측 - 관측.T).max():>16.4f}")
print(f"{'무한':>10} {np.abs(D - D.T).max():>16.4f}   <- 0 으로 안 간다")
      표본 수     |D - D^T| 최대
        50           0.7599
       500           0.5086
      5000           0.5076
     50000           0.5019
        무한           0.5000   <- 0 으로 안 간다

2. 시험 하나가 총량의 절반을 쥔다 (문제 48)

def 지렛대(부하):
    A = np.c_[np.ones(len(부하)), np.asarray(부하, dtype=float)]
    return np.diag(A @ np.linalg.inv(A.T @ A) @ A.T)


h = 지렛대([1, 2, 3, 6])
같다("48", h, np.array([30, 18, 14, 50]) / 56, "지렛대값")
같다("48", h.sum(), 2.0, "합 = 랭크 = 2")
print(f"     넷째가 총량의 {h[3]/h.sum():.1%}")

print()
print(f"{'부하':>18} {'h_ii':>28} {'합':>6} {'최대':>7}")
for 부하 in ([1, 2, 3, 6], [1, 2, 3, 4], [1, 2, 3, 4, 6], [1, 2, 3, 4, 5, 6]):
    hh = 지렛대(부하)
    print(f"{str(부하):>18} {str(np.round(hh, 3)):>28} {hh.sum():>6.1f} {hh.max():>7.3f}")
print("  합은 늘 2 다. 나누는 방식만 고를 수 있다.")
[48] 맞다   차이 2.22e-16   지렛대값
[48] 맞다   차이 8.88e-16   합 = 랭크 = 2
     넷째가 총량의 44.6%

                부하                         h_ii      합      최대
      [1, 2, 3, 6]    [0.536 0.321 0.25  0.893]    2.0   0.893
      [1, 2, 3, 4]            [0.7 0.3 0.3 0.7]    2.0   0.700
   [1, 2, 3, 4, 6] [0.527 0.297 0.203 0.243 0.73 ]    2.0   0.730
[1, 2, 3, 4, 5, 6] [0.524 0.295 0.181 0.181 0.295 0.524]    2.0   0.524
  합은 늘 2 다. 나누는 방식만 고를 수 있다.

지렛대값이 큰 점은 잔차가 작게 나온다. 그래서 틀려도 안 보인다. 직접 확인해 보자.

부하 = np.array([1.0, 2, 3, 6])
A = np.c_[np.ones(4), 부하]
참 = 10 + 2 * 부하                              # 참 관계
h = 지렛대(부하)

print(f"{'어느 점을 5만큼 흔드나':>24} {'그 점의 잔차':>12} {'h_ii':>8}")
for k in range(4):
    y = 참.copy()
    y[k] += 5
    be = np.linalg.solve(A.T @ A, A.T @ y)
    e = y - A @ be
    print(f"{f'{k+1}번 (부하 {부하[k]:.0f})':>24} {e[k]:>12.3f} {h[k]:>8.3f}")
print("  h_ii 가 클수록 잔차가 작다. 5만큼 틀렸는데 0.5 밖에 안 남는다.")
같다("48", 1 - h, [abs(5 * (1 - h[k])) / 5 for k in range(4)],
    "잔차 = (1 - h_ii) x 흔든 양")
           어느 점을 5만큼 흔드나      그 점의 잔차     h_ii
               1번 (부하 1)        2.321    0.536
               2번 (부하 2)        3.393    0.321
               3번 (부하 3)        3.750    0.250
               4번 (부하 6)        0.536    0.893
  h_ii 가 클수록 잔차가 작다. 5만큼 틀렸는데 0.5 밖에 안 남는다.
[48] 맞다   차이 0.00e+00   잔차 = (1 - h_ii) x 흔든 양

3. 선행 주소행렬식이 놓치는 것 (문제 49)

mS = 3 * np.eye(3) - np.ones((3, 3))
선행 = [np.linalg.det(mS[:k, :k]) for k in (1, 2, 3)]
같다("49", 선행, [2, 3, 0], "선행 주소행렬식")
같다("49", np.sort(np.linalg.eigvalsh(mS)), [0, 3, 3], "고윳값")

모두 = []
for k in (1, 2, 3):
    for idx in itertools.combinations(range(3), k):
        모두.append(round(float(np.linalg.det(mS[np.ix_(idx, idx)])), 9))
print(f"     모든 주소행렬식 {sorted(모두)}  (2^3 - 1 = {2**3-1} 개)")
print(f"     전부 0 이상인가 : {all(v >= -1e-9 for v in 모두)}")

print()
print("반례 : 선행은 통과하는데 준양정부호가 아닌 표")
K = np.array([[0.0, 0], [0, -1]])
print(f"     K = {K.tolist()}")
print(f"     선행 주소행렬식 {[np.linalg.det(K[:k, :k]) for k in (1, 2)]}  -> 음수 없음")
print(f"     고윳값 {np.linalg.eigvalsh(K)}  -> 준양정부호가 아니다")
모두K = [round(float(np.linalg.det(K[np.ix_(i, i)])), 9)
        for k in (1, 2) for i in itertools.combinations(range(2), k)]
print(f"     모든 주소행렬식 {sorted(모두K)}  -> -1 이 있어 걸린다")
[49] 맞다   차이 4.44e-16   선행 주소행렬식
[49] 맞다   차이 1.11e-16   고윳값
     모든 주소행렬식 [0.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0]  (2^3 - 1 = 7 개)
     전부 0 이상인가 : True

반례 : 선행은 통과하는데 준양정부호가 아닌 표
     K = [[0.0, 0.0], [0.0, -1.0]]
     선행 주소행렬식 [np.float64(0.0), np.float64(0.0)]  -> 음수 없음
     고윳값 [-1.  0.]  -> 준양정부호가 아니다
     모든 주소행렬식 [-1.0, 0.0, 0.0]  -> -1 이 있어 걸린다

2n12^n - 1 개를 다 보는 것은 실무에서 불가능하다. 고윳값이나 피벗을 쓰자.

print(f"{'n':>4} {'모든 주소행렬식 개수':>20} {'고윳값 개수':>12}")
for n in (3, 5, 10, 20):
    print(f"{n:>4} {2**n - 1:>20} {n:>12}")
   n          모든 주소행렬식 개수       고윳값 개수
   3                    7            3
   5                   31            5
  10                 1023           10
  20              1048575           20

4. 봉우리는 실험 범위 밖에 있었다 (문제 50)

def 수율(v, 교차):
    v = np.asarray(v, dtype=float)
    return 80 + 6 * v[1] - 2 * v[0] ** 2 - 2 * v[1] ** 2 + 교차 * v[0] * v[1]


for 교차 in (2.0, 6.0):
    B = np.array([[-2.0, 교차 / 2], [교차 / 2, -2]])
    xs = np.linalg.solve(2 * B, -np.array([0.0, 6]))
    w = np.linalg.eigvalsh(B)
    성격 = "봉우리" if (w < 0).all() else "안장"
    print(f"교차항 {교차:.0f} : B = {B.tolist()},  고윳값 {w},  "
          f"정류점 {np.round(xs, 3)},  y = {수율(xs, 교차):.2f}   -> {성격}")
print("  정류점도 옮겨 가고 성격도 바뀐다. 대각은 둘 다 (-2,-2) 로 그대로다.")

B = np.array([[-2.0, 1], [1, -2]])
xs = np.array([1.0, 2])
같다("50", xs, np.linalg.solve(2 * B, -np.array([0.0, 6])), "정류점 (1,2)")
같다("50", 수율(xs, 2.0), 86.0, "y* = 86")
교차항 2 : B = [[-2.0, 1.0], [1.0, -2.0]],  고윳값 [-3. -1.],  정류점 [1. 2.],  y = 86.00   -> 봉우리
교차항 6 : B = [[-2.0, 3.0], [3.0, -2.0]],  고윳값 [-5.  1.],  정류점 [-1.8 -1.2],  y = 76.40   -> 안장
  정류점도 옮겨 가고 성격도 바뀐다. 대각은 둘 다 (-2,-2) 로 그대로다.
[50] 맞다   차이 0.00e+00   정류점 (1,2)
[50] 맞다   차이 0.00e+00   y* = 86
# 능선 : 방향마다 떨어지는 속도가 lambda 다
w, V = np.linalg.eigh(B)
print(f"{'방향':>12} {'v^T B v':>10} {'lambda |v|^2':>14}")
for k in range(2):
    v = V[:, k]
    print(f"{str(np.round(v, 3)):>12} {v @ B @ v:>10.4f} {w[k] * (v @ v):>14.4f}")
print(f"  느린 방향 {np.round(V[:, int(np.argmax(w))], 3)} 이 능선이다")

print()
print("실험 범위 [-1,1]^2 안에서 가장 좋았던 점과 모형이 가리키는 봉우리 :")
격 = np.linspace(-1, 1, 41)
최고 = max(((수율((a, b), 2.0), (a, b)) for a in 격 for b in 격))
print(f"  범위 안 최고 : {최고[1]} 에서 {최고[0]:.2f}")
print(f"  모형의 봉우리 : (1, 2) 에서 86.00   <- 압력 2 는 시험한 적이 없다")
          방향    v^T B v   lambda |v|^2
[ 0.707 -0.707]    -3.0000        -3.0000
[0.707 0.707]    -1.0000        -1.0000
  느린 방향 [0.707 0.707] 이 능선이다

실험 범위 [-1,1]^2 안에서 가장 좋았던 점과 모형이 가리키는 봉우리 :
  범위 안 최고 : (np.float64(0.5), np.float64(1.0)) 에서 84.50
  모형의 봉우리 : (1, 2) 에서 86.00   <- 압력 2 는 시험한 적이 없다

5. 열 합 규칙은 충분조건일 뿐이다 (문제 51)

표 = {"주 표": [[.5, .1, .3], [.2, .5, .1], [.1, .2, .4]],
      "잘못 입력한 표": [[.5, .1, .3], [.2, .5, .1], [.6, .6, .4]],
      "열 합이 1 을 넘는 표": [[.5, .1, .3], [.2, .5, .1], [.4, .5, .4]]}
print(f"{'표':>22} {'열 합':>18} {'선행 주소행렬식':>26} {'rho':>8} {'성립':>6}")
for 이름, M in 표.items():
    A = np.array(M)
    IA = np.eye(3) - A
    선행 = [np.linalg.det(IA[:k, :k]) for k in (1, 2, 3)]
    rho = max(abs(np.linalg.eigvals(A)))
    성립 = all(v > 0 for v in 선행)
    print(f"{이름:>22} {str(np.round(A.sum(axis=0), 2)):>18} "
          f"{str(np.round(선행, 4)):>26} {rho:>8.4f} {str(성립):>6}")

print()
print("열 합이 rho 를 가두는 범위 (성분이 음이 아닐 때) :")
for 이름, M in 표.items():
    A = np.array(M)
    열 = A.sum(axis=0)
    rho = max(abs(np.linalg.eigvals(A)))
    print(f"  {이름:>22} : {열.min():.2f} <= {rho:.4f} <= {열.max():.2f}  "
          f"{'참' if 열.min() - 1e-9 <= rho <= 열.max() + 1e-9 else '거짓'}")
print("  -> 최소가 1 을 넘어야 rho > 1 이 확정된다. 일부만 넘는 것으로는 모른다.")
                     표                열 합                   선행 주소행렬식      rho     성립
                   주 표      [0.8 0.8 0.8]           [0.5  0.23 0.1 ]   0.8000   True
              잘못 입력한 표      [1.3 1.2 0.8]     [ 0.5    0.23  -0.024]   1.0369  False
         열 합이 1 을 넘는 표      [1.1 1.1 0.8]        [0.5   0.23  0.019]   0.9689   True

열 합이 rho 를 가두는 범위 (성분이 음이 아닐 때) :
                     주 표 : 0.80 <= 0.8000 <= 0.80  참
                잘못 입력한 표 : 0.80 <= 1.0369 <= 1.30  참
           열 합이 1 을 넘는 표 : 0.80 <= 0.9689 <= 1.10  참
  -> 최소가 1 을 넘어야 rho > 1 이 확정된다. 일부만 넘는 것으로는 모른다.

6. 대각합이 같아도 갈린다 (문제 52, 54)

e0 = np.array([2.0, 0])
print("이산 : e_(t+1) = M e_t")
for 이름, M in [("갑", [[.4, .3], [.3, .4]]), ("을", [[.4, .8], [.8, .4]])]:
    M = np.array(M)
    w = np.linalg.eigvalsh(M)
    e5 = np.linalg.matrix_power(M, 5) @ e0
    print(f"  {이름} : 대각 {np.diag(M)}  대각합 {np.trace(M):.1f}  det {np.linalg.det(M):+.2f}  "
          f"고윳값 {w}  5년 뒤 {np.round(e5, 4)}")
같다("52", np.linalg.matrix_power(np.array([[.4, .3], [.3, .4]]), 5) @ e0,
    [0.7**5, 0.7**5], "갑은 0.7^5 로 줄어든다", 허용=1e-4)
print("  대각합이 둘 다 0.8 이다. 가른 것은 행렬식이다.")
이산 : e_(t+1) = M e_t
  갑 : 대각 [0.4 0.4]  대각합 0.8  det +0.07  고윳값 [0.1 0.7]  5년 뒤 [0.1681 0.1681]
  을 : 대각 [0.4 0.4]  대각합 0.8  det -0.48  고윳값 [-0.4  1.2]  5년 뒤 [2.4781 2.4986]
[52] 맞다   차이 1.00e-05   갑은 0.7^5 로 줄어든다
  대각합이 둘 다 0.8 이다. 가른 것은 행렬식이다.
print("연속 : dp/dt = J p,  세 도시 모두 대각 (-2,-2), 대각합 -4")
for 이름, J in [("(가)", [[-2., 1], [1, -2]]), ("(나)", [[-2., -3], [3, -2]]),
               ("(다)", [[-2., 9], [1, -2]])]:
    J = np.array(J)
    w = np.linalg.eigvals(J)
    판별 = np.trace(J) ** 2 - 4 * np.linalg.det(J)
    거동 = ("발산" if max(np.real(w)) > 0
           else ("나선 수렴" if abs(np.imag(w)).max() > 1e-9 else "단조 수렴"))
    print(f"  {이름} det {np.linalg.det(J):+6.1f}  판별식 {판별:+7.1f}  "
          f"고윳값 {np.round(w, 3)}  -> {거동}")
print("  대각합이 같으니 남은 자유도는 행렬식 하나뿐이다.")

from scipy.linalg import expm
print()
print(f"{'t':>5} {'(가) 거리':>11} {'(나) 거리':>11} {'(다) 거리':>13}")
p0 = np.array([1.0, 0])
for t in (0.5, 1, 2, 4):
    줄 = [np.linalg.norm(expm(np.array(J) * t) @ p0)
          for J in ([[-2., 1], [1, -2]], [[-2., -3], [3, -2]], [[-2., 9], [1, -2]])]
    print(f"{t:>5.1f} {줄[0]:>11.5f} {줄[1]:>11.5f} {줄[2]:>13.4f}")
연속 : dp/dt = J p,  세 도시 모두 대각 (-2,-2), 대각합 -4
  (가) det   +3.0  판별식    +4.0  고윳값 [-1.+0.j -3.+0.j]  -> 단조 수렴
  (나) det  +13.0  판별식   -36.0  고윳값 [-2.+3.j -2.-3.j]  -> 나선 수렴
  (다) det   -5.0  판별식   +36.0  고윳값 [ 1.+0.j -5.+0.j]  -> 발산
  대각합이 같으니 남은 자유도는 행렬식 하나뿐이다.

    t      (가) 거리      (나) 거리        (다) 거리
  0.5     0.45698     0.36788        0.9039
  1.0     0.26250     0.13534        1.4355
  2.0     0.09571     0.01832        3.8944
  4.0     0.01295     0.00034       28.7758

7. 조건수가 계수를 흔든다 (문제 53)

z1 = np.array([3.0, 4, 0, -4, -3])
z2 = np.array([4.0, 3, 0, -3, -4])
Z = np.c_[z1, z2]
G = Z.T @ Z
같다("53", G, [[50, 48], [48, 50]], "Z^T Z")
같다("53", np.sort(np.linalg.eigvalsh(G)), [2, 98], "고윳값 2, 98")
s = np.linalg.svd(Z)[1]
같다("53", s, [7 * np.sqrt(2), np.sqrt(2)], "특이값")
같다("53", np.linalg.cond(Z), 7.0, "kappa(Z) = 7")
같다("53", np.linalg.cond(G), 49.0, "kappa(Z^T Z) = 49")
같다("53", s ** 2, np.linalg.svd(G)[1], "sigma(Z^T Z) = sigma(Z)^2")

print()
print(f"{'우변':>16} {'합':>6} {'차':>6} {'계수':>16}")
for rhs in ([98., 98], [100., 96], [99., 97], [102., 94]):
    be = np.linalg.solve(G, rhs)
    print(f"{str(rhs):>16} {rhs[0]+rhs[1]:>6.0f} {rhs[0]-rhs[1]:>6.0f} "
          f"{str(np.round(be, 3)):>16}")
print("  합은 거의 안 움직이는데 차만 벌어진다")
print(f"  합은 98 로 나뉘고 차는 2 로 나뉜다 -> {98/2:.0f}배 예민하다")
[53] 맞다   차이 0.00e+00   Z^T Z

[53] 맞다   차이 3.55e-15   고윳값 2, 98
[53] 맞다   차이 2.22e-16   특이값
[53] 맞다   차이 1.78e-15   kappa(Z) = 7
[53] 맞다   차이 2.34e-13   kappa(Z^T Z) = 49
[53] 맞다   차이 1.42e-14   sigma(Z^T Z) = sigma(Z)^2

              우변      합      차               계수
      [98.0, 98]    196      0          [1. 1.]
     [100.0, 96]    196      4          [2. 0.]
      [99.0, 97]    196      2        [1.5 0.5]
     [102.0, 94]    196      8        [ 3. -1.]
  합은 거의 안 움직이는데 차만 벌어진다
  합은 98 로 나뉘고 차는 2 로 나뉜다 -> 49배 예민하다

8. 최소제곱이 강제하는 직교 (문제 56)

여기가 이 편에서 제일 중요한 셀이다.

x = np.array([3.0, 1, -1, -3])
v = np.array([1.0, -1, 1, -1])          # 숨은 요인 (현실에서는 안 보인다)
z = np.array([1.0, 1, -1, -1])          # 도구
y = 2 * x + v

같다("56", y, [7, 1, -1, -7], "관측된 사용량")
be = x @ y / (x @ x)
같다("56", be, 2.2, "통상 계산 (참값 2 보다 위)")
같다("56", be, 2 + (x @ v) / (x @ x), "편의 = x.v / x.x")
print(f"     x.v = {x @ v:.0f}  <- 0 이 아닌 것이 편의의 정체")

e = y - be * x
같다("56", x @ e, 0.0, "잔차가 x 와 직교")
print("     -> 그런데 이 직교는 be 를 그렇게 골랐기 때문이다")
[56] 맞다   차이 0.00e+00   관측된 사용량
[56] 맞다   차이 0.00e+00   통상 계산 (참값 2 보다 위)
[56] 맞다   차이 0.00e+00   편의 = x.v / x.x
     x.v = 4  <- 0 이 아닌 것이 편의의 정체
[56] 맞다   차이 3.55e-15   잔차가 x 와 직교
     -> 그런데 이 직교는 be 를 그렇게 골랐기 때문이다
# 어떤 자료를 넣어도 직교가 나온다는 것을 확인한다
난 = np.random.default_rng(56)
어긋남 = 0
for _ in range(20000):
    xx = 난.normal(size=6)
    yy = 난.normal(size=6)                # 아무 관계도 없는 자료
    b = xx @ yy / (xx @ xx)
    if abs(xx @ (yy - b * xx)) > 1e-9:
        어긋남 += 1
print(f"무작위 자료 2만 쌍 중 잔차가 직교하지 않은 경우 : {어긋남}개")
print("  -> 직교는 모형의 증거가 아니다. 나눗셈을 제대로 했다는 뜻뿐이다.")
무작위 자료 2만 쌍 중 잔차가 직교하지 않은 경우 : 0개
  -> 직교는 모형의 증거가 아니다. 나눗셈을 제대로 했다는 뜻뿐이다.
# 도구변수가 참값을 되찾는다
같다("56", z @ y / (z @ x), 2.0, "도구로 계산")
같다("56", z @ v, 0.0, "도구가 숨은 요인과 직교")
print(f"     z.x = {z @ x:.0f}  <- 0 이 아니어야 나눗셈이 된다")

xh = z * (z @ x) / (z @ z)
같다("56", xh, [2, 2, -2, -2], "1단계 : x 를 z 위로 사영")
같다("56", xh @ y / (xh @ xh), 2.0, "2단계도 같은 답")

zp = np.array([1.0, -1, -1, 1])
print(f"\n     잘못 고른 도구 z' : z'.x = {zp @ x:.0f}  -> 나눗셈 불가")
[56] 맞다   차이 0.00e+00   도구로 계산
[56] 맞다   차이 0.00e+00   도구가 숨은 요인과 직교
     z.x = 8  <- 0 이 아니어야 나눗셈이 된다
[56] 맞다   차이 0.00e+00   1단계 : x 를 z 위로 사영
[56] 맞다   차이 0.00e+00   2단계도 같은 답

     잘못 고른 도구 z' : z'.x = 0  -> 나눗셈 불가

9. 유사변환은 스펙트럼만 지킨다 (문제 57)

x3 = np.array([420.0, 320, 260])
Z3 = np.array([[210.0, 32, 78], [84, 160, 26], [42, 64, 104]])
A3 = Z3 @ np.diag(1 / x3)
B3 = np.diag(1 / x3) @ Z3

같다("57", A3, [[.5, .1, .3], [.2, .5, .1], [.1, .2, .4]], "레온티예프 A")
같다("57", B3, np.diag(1 / x3) @ A3 @ np.diag(x3), "B = xhat^-1 A xhat (유사변환)")
같다("57", np.sort(abs(np.linalg.eigvals(A3))), np.sort(abs(np.linalg.eigvals(B3))),
    "고윳값이 같다")

print(f"\n{'':>16} {'대각합':>10} {'행렬식':>12} {'랭크':>6}")
for 이름, M in (("A", A3), ("B", B3)):
    print(f"{이름:>16} {np.trace(M):>10.4f} {np.linalg.det(M):>12.6f} "
          f"{np.linalg.matrix_rank(M):>6}")
print("  -> 유사변환이 지키는 것들")

print(f"\n{'':>16} {'열 합':>26} {'행 합':>26}")
for 이름, M in (("A", A3), ("B", B3)):
    print(f"{이름:>16} {str(np.round(M.sum(axis=0), 4)):>26} "
          f"{str(np.round(M.sum(axis=1), 4)):>26}")
print("  -> A 의 열 합은 균일한데 B 의 행 합은 아니다. 지키지 않는 것들")
print(f"  A12 = {A3[0,1]:.4f},  B12 = {B3[0,1]:.4f}  <- 성분도 다르다")
[57] 맞다   차이 5.55e-17   레온티예프 A
[57] 맞다   차이 5.55e-17   B = xhat^-1 A xhat (유사변환)
[57] 맞다   차이 1.33e-15   고윳값이 같다

                        대각합          행렬식     랭크
               A     1.4000     0.080000      3
               B     1.4000     0.080000      3
  -> 유사변환이 지키는 것들

                                        열 합                        행 합
               A              [0.8 0.8 0.8]              [0.9 0.8 0.7]
               B     [0.924  0.8223 0.667 ]     [0.7619 0.8438 0.8077]
  -> A 의 열 합은 균일한데 B 의 행 합은 아니다. 지키지 않는 것들
  A12 = 0.1000,  B12 = 0.0762  <- 성분도 다르다

10. 왜 0.5회가 나오나 (문제 59)

def 부분행렬식(M):
    n = len(M)
    값 = set()
    for k in range(1, n + 1):
        for r in itertools.combinations(range(n), k):
            for c in itertools.combinations(range(n), k):
                값.add(round(float(np.linalg.det(M[np.ix_(r, c)]))))
    return sorted(값)


M3 = np.array([[1.0, 0, 1], [1, 1, 0], [0, 1, 1]])        # 삼각형 (홀수 사이클)
M4 = np.array([[1.0, 0, 0, 1], [1, 1, 0, 0],
               [0, 1, 1, 0], [0, 0, 1, 1]])                # 4-사이클 (짝수)
같다("59", np.linalg.det(M3), 2.0, "삼각형의 행렬식")
같다("59", np.linalg.solve(M3, np.ones(3)), [.5, .5, .5], "답이 0.5 회")
print(f"     3-사이클 부분행렬식 {부분행렬식(M3)}  <- 2 가 있다")
print(f"     4-사이클 부분행렬식 {부분행렬식(M4)}  <- 0, +-1 뿐 (완전단모듈)")

print()
print(f"{'사이클 길이':>12} {'det':>6} {'부분행렬식':>16} {'해':>26}")
for n in range(3, 8):
    M = np.eye(n) + np.eye(n, k=-1)
    M[0, -1] = 1
    if abs(np.linalg.det(M)) < 1e-9:
        print(f"{n:>12} {0:>6} {'특이':>16} {'유일하지 않다':>26}")
        continue
    해 = np.linalg.solve(M, np.ones(n))
    print(f"{n:>12} {np.linalg.det(M):>6.0f} {str(부분행렬식(M)):>16} "
          f"{str(np.round(해, 3)):>26}")
print("  홀수 사이클에서만 0.5 가 나온다")
print()
print("짝수 사이클은 특이행렬이라 해가 유일하지 않다. 그런데 정수해는 있다 :")
M4b = np.array([[1.0, 0, 0, 1], [1, 1, 0, 0], [0, 1, 1, 0], [0, 0, 1, 1]])
후보 = np.array([1.0, 0, 1, 0])
print(f"  M4 x = {M4b @ 후보}  일 때  x = {후보.astype(int)}")
print("  완전단모듈이 보장하는 것은 '유일한 해'가 아니라 '해가 있으면 정수'다.")
[59] 맞다   차이 0.00e+00   삼각형의 행렬식
[59] 맞다   차이 0.00e+00   답이 0.5 회
     3-사이클 부분행렬식 [-1, 0, 1, 2]  <- 2 가 있다
     4-사이클 부분행렬식 [-1, 0, 1]  <- 0, +-1 뿐 (완전단모듈)

      사이클 길이    det            부분행렬식                          해
           3      2    [-1, 0, 1, 2]              [0.5 0.5 0.5]
           4      0               특이                    유일하지 않다
           5      2    [-1, 0, 1, 2]      [0.5 0.5 0.5 0.5 0.5]
           6      0               특이                    유일하지 않다
           7      2    [-1, 0, 1, 2] [0.5 0.5 0.5 0.5 0.5 0.5 0.5]
  홀수 사이클에서만 0.5 가 나온다

짝수 사이클은 특이행렬이라 해가 유일하지 않다. 그런데 정수해는 있다 :
  M4 x = [1. 1. 1. 1.]  일 때  x = [1 0 1 0]
  완전단모듈이 보장하는 것은 '유일한 해'가 아니라 '해가 있으면 정수'다.

11. 같은 표, 반대 결론 (문제 60)

A = np.array([[-0.5, 1.0], [1.0, -0.5]])
d = np.array([1.0, 1])
w = np.linalg.eigvalsh(A)
같다("60", np.sort(w), [-1.5, 0.5], "고윳값")

고정 = np.linalg.solve(np.eye(2) - A, d)
같다("60", 고정, [2, 2], "고정점")
print(f"     이산 조건 max|lambda| = {max(abs(w)):.1f} < 1 ?  "
      f"{'수렴' if max(abs(w)) < 1 else '발산'}")
print(f"     연속 조건 max Re(lambda) = {max(w):.1f} < 1 ?  "
      f"{'수렴' if max(w) < 1 else '발산'}")

from scipy.linalg import expm
x0 = np.array([2.0, 0])
xi = x0.copy()
print(f"\n{'t':>4} {'이산 거리':>12} {'연속 거리':>12}")
for t in range(1, 9):
    xi = A @ xi + d
    xc = expm((A - np.eye(2)) * t) @ (x0 - 고정) + 고정
    if t in (1, 2, 4, 6, 8):
        print(f"{t:>4} {np.abs(xi - 고정).max():>12.4f} "
              f"{np.abs(xc - 고정).max():>12.6f}")
print("  같은 표인데 결론이 반대다. 갱신 방식이 다르기 때문이다.")
[60] 맞다   차이 0.00e+00   고윳값
[60] 맞다   차이 2.22e-16   고정점
     이산 조건 max|lambda| = 1.5 < 1 ?  발산
     연속 조건 max Re(lambda) = 0.5 < 1 ?  수렴

   t        이산 거리        연속 거리
   1       2.0000     0.688616
   2       2.5000     0.374617
   4       5.1250     0.135381
   6      11.4062     0.049787
   8      25.6328     0.018316
  같은 표인데 결론이 반대다. 갱신 방식이 다르기 때문이다.
# 두 조건이 복소평면에서 어떤 영역인지
print("이산은 원 안(|lambda| < 1), 연속은 수직선 왼쪽(Re lambda < 1)")
print(f"{'lambda':>16} {'|lambda|':>10} {'Re':>7} {'이산':>6} {'연속':>6}")
for lam in (0.5, -1.5, 0.9, -0.9, 1.5, complex(0, 2), complex(-0.5, 0.5)):
    이산 = "수렴" if abs(lam) < 1 else "발산"
    연속 = "수렴" if np.real(lam) < 1 else "발산"
    print(f"{str(lam):>16} {abs(lam):>10.3f} {np.real(lam):>7.2f} "
          f"{이산:>6} {연속:>6}")
print("  원은 수직선 왼쪽에 들어간다 -> 이산이 수렴하면 연속도 수렴한다")
print("  반대는 성립하지 않는다. 그 틈이 이 문제다.")
이산은 원 안(|lambda| < 1), 연속은 수직선 왼쪽(Re lambda < 1)
          lambda   |lambda|      Re     이산     연속
             0.5      0.500    0.50     수렴     수렴
            -1.5      1.500   -1.50     발산     수렴
             0.9      0.900    0.90     수렴     수렴
            -0.9      0.900   -0.90     수렴     수렴
             1.5      1.500    1.50     발산     발산
              2j      2.000    0.00     발산     수렴
     (-0.5+0.5j)      0.707   -0.50     수렴     수렴
  원은 수직선 왼쪽에 들어간다 -> 이산이 수렴하면 연속도 수렴한다
  반대는 성립하지 않는다. 그 틈이 이 문제다.

12. 예순 문제가 서른일곱 강의를 어떻게 되짚었나

원고에서 직접 읽어 센다. 손으로 적은 숫자를 쓰면 원고를 고칠 때 표가 조용히 거짓말을 한다.

import collections
import pathlib
import re

뿌리 = pathlib.Path.cwd()
while not (뿌리 / "lectures").exists() and 뿌리 != 뿌리.parent:
    뿌리 = 뿌리.parent

셈 = collections.Counter()
문제수 = 0
for 편 in ("L38", "L39", "L40"):
    p = 뿌리 / "lectures" / 편 / f"{편}_theory.md"
    본문 = p.read_text(encoding="utf-8")
    조각 = 본문.split("## 이 편의 회수표")
    for 줄 in 조각[1].splitlines():
        if not 줄.startswith("|") or 줄.count("|") < 5:
            continue
        칸 = [c.strip() for c in 줄.strip().strip("|").split("|")]
        if not re.match(r"^\[\d+\]\(#prob-", 칸[0]):
            continue                      # 전체 색인 표가 섞이지 않게
        문제수 += 1
        for n in re.findall(r"L(\d{2})", 칸[-1]):
            셈[int(n)] += 1

print(f"문제 {문제수}개,  회수 {sum(셈.values())}건,  닿은 강의 {len(셈)}/37")
print(f"한 번도 안 나온 강의 : {[k for k in range(1, 38) if k not in 셈]}")
print()
print("많이 불린 것 열 개")
for 강의, n in sorted(셈.items(), key=lambda t: (-t[1], t[0]))[:10]:
    print(f"  L{강의:02d} {'#' * n} {n}")
문제 60개,  회수 195건,  닿은 강의 33/37
한 번도 안 나온 강의 : [13, 31, 32, 37]

많이 불린 것 열 개
  L03 ################ 16
  L21 ############# 13
  L09 ############ 12
  L16 ############ 12
  L07 ########## 10
  L25 ########## 10
  L27 ########## 10
  L06 ######## 8
  L10 ######## 8
  L05 ####### 7
막 = [(1, 4, "1막 계산"), (5, 13, "2막 공간"), (14, 17, "3막 각도"),
      (18, 20, "4막 행렬식"), (21, 25, "5막 고윳값"), (26, 29, "6막 조건 버리기"),
      (30, 34, "7막 변환"), (35, 37, "8막 보강")]
print(f"{'막':>18} {'강의 수':>8} {'회수':>6} {'강의당':>8}")
for 시, 끝, 이름 in 막:
    n = sum(셈.get(k, 0) for k in range(시, 끝 + 1))
    개 = 끝 - 시 + 1
    print(f"{이름:>18} {개:>8} {n:>6} {n/개:>8.1f}")
                 막     강의 수     회수      강의당
             1막 계산        4     24      6.0
             2막 공간        9     61      6.8
             3막 각도        4     29      7.2
            4막 행렬식        3     17      5.7
            5막 고윳값        5     39      7.8
         6막 조건 버리기        4     17      4.2
             7막 변환        5      4      0.8
             8막 보강        3      4      1.3

정리

확인한 것결과
관측표가 대칭에 가까워지나아니다. 표본을 늘려도 어긋남이 그대로
지렛대값의 합늘 랭크 2. 나누는 방식만 고를 수 있다
지렛대가 큰 점을 5만큼 흔들면잔차가 (1hii)×5(1-h_{ii})\times5 만 남는다
선행 주소행렬식으로 준정부호 판정못 한다. 2n12^n-1 개를 다 봐야 한다
교차항을 키우면정류점이 옮겨 가고 성격도 안장으로 바뀐다
열 합이 ρ\rho 를 가두나minρmax\min \le \rho \le \max. 일부만 넘는 것으로는 모른다
대각합이 같은 두 행렬하나는 0.168로 수렴, 하나는 2.48로 발산
대각도 대각합도 같은 세 행렬단조 수렴 / 나선 수렴 / 발산
σ(ZTZ)=σ(Z)2\sigma(Z^{\mathsf T}Z) = \sigma(Z)^2성립. 조건수가 7에서 49로
잔차가 직교하는가무작위 자료 2만 쌍 전부 직교했다
유사변환이 지키는 것고윳값·대각합·행렬식·랭크. 성분과 합계는 아니다
홀수 사이클부분행렬식에 2가 생겨 정수해가 깨진다
같은 표, 이산과 연속하나는 25.6으로 발산, 하나는 0.02로 수렴
예순 문제의 회수원고에서 읽어 센다

열째 줄이 이 노트북의 결론이다.

무작위로 만든 아무 관계 없는 자료 2만 쌍에서도 잔차는 한 번도 빠짐없이 설명변수와 직교했다. 그러니 직교를 확인하는 것은 모형을 확인하는 것이 아니다.

계산이 강제하는 것과 자료가 말해 주는 것을 구별하는 일 — 그것이 이 편의 전부였다.