3년 간 방치한 아킬레스건염을 꾸준한 재활 운동으로 극복한지 어연 2년째.
재활 운동 과정을 기록해 둔 데이터를 시각화하면서 특별하게 회고해봅니다!
이번 포스팅을 통해 알 수 있는 팁
•
datetime을 통해 duration 구하기 (.shift 메소드)
•
오랜만에 seaborn이 아닌 matplotlib으로 그래프 그리기
•
판다스 .loc .iloc 자유자재로 사용하기
아킬레스건염, 그 지긋지긋한 역사 
아킬레스건염 : 충격을 받으면 찌릿찌릿 아프고 오래 걷기 불편하다.
저는 과거 군사 훈련 동안 사이즈가 맞지 않은 군화를 배정 받은 상태로 행군 등의 장거리 걷기 활동을 소화해야 했습니다. 이런 영향으로 오른쪽 발에 아킬레스건염이 발발하고, 빠른 치료를 하지 못하고 점점 악화되고 맙니다.
이후 모 정형외과에서 아킬레스건에 직접 스테로이드 주사를 맞는, 잘못된 처치를 2회 맞은 후 아킬레스건염은 더욱 심각해지고 맙니다.
그렇게 약 3년 이상 바쁘다는 핑계로 참고 살다가, 본격적인 재활 운동 치료를 시작하게 됩니다.
출처 : 유튜브 [Mr. Physio 호주물리치료사] 채널의 아킬레스건염 재활 운동 영상
운동은 계단식 바닥을 발 앞부분으로 밟아 아킬레스건을 쭉 펴주는 운동을 반복하는 것입니다.
아주 간단하지만 인간에게 가장 어려운, '꾸준히 반복해서' 해야하는 운동이었습니다.
꾸준한 재활 운동, 기록하는 재미로 극복하다 
네?! 하루에 두 번씩 약 20분 정도 시간을 내서 꾸준하게 운동을 하라고요...?!
저는 퀘스트를 깨는 심정으로, 계단 밟기 운동을 5번 씩 하루 두 번 할 때마다 행동 기록 어플리케이션에 기록하며 성실하게 수행해갔습니다.
이때 5번을 채우면 한 칸씩 체크할 수 있는 어플리케이션, STREAKS을 사용했는데, STREAKS은 애플워치에서도 기록을 할 수 있어서 손쉽게 기록을 할 수 있어서 좋았습니다. ⌚️
괜찮아 나에겐 애플워치가 있으니까...!
그렇게 5개월 간 거의 하루도 빠짐 없이 아침 저녁으로 재활 운동을 해냈고, 3년 넘게 저를 괴롭혔던 아킬레스건염에서 완치될 수 있었습니다.
처음에는 "이게 효과가 있을까" 싶었지만, 하루 하루 꾸준히 - 그리고 지루하게 재활 운동을 해 내자 3개월차부터 통증이 줄고 혈색도 바뀌고 5개월차부터는 멀쩡한 왼발과 느낌이 같아졌습니다.
데이터... 데이터를 보자! 
STREAKS 애플워치 앱 캡쳐
STREAKS 어플은 애플워치로 기록한 달성 데이터를 아이폰을 통해 csv로 추출할 수 있었습니다.
CSV에는 언제 목표한 행동을 달성했는지가 기록되어 있었습니다.
completed_manually_partial 은 5번의 아킬레스건 운동 셋트 중 1/5을 달성했다는 의미입니다.
이런식으로 운동을 완수한 일시 데이터가 있는 반면 (entry_timestamp), 중요하지 않은 데이터 항목들도 있군요 (display order 등)
데이터 전처리를 통해 원하는 데이터를 추려봅시다.
•
언제 운동을 완수하였는가? → 아킬레스건 운동을 평소 언제쯤 했는지를 파악하는데 사용
•
운동의 첫 마무리 (1/5 회) 와 끝 마무리(5/5 회)의 시각은 언제인가? → 운동 당 평균 소요시간 측정
그러면 아무래도 entry_timestamp 의 전처리가 가장 중요한 문제가 될 것 같습니다.
평소에 Pandas 패키지에서 빈용되는 양식의 timestamp 데이터 꼴이 아니다 보니 섬세한 처리가 필요합니다.
import pandas as pd # 우리의 친구 판다스
df = df[['entry_type', 'entry_date', 'entry_timestamp']] # 필요한 칼럼만 뽑습니다
df.columns = ['type', 'date', 'timestamp'] # 칼럼 이름을 간편하게 바꿔줍니다
df['timestamp'] = df['timestamp'].str.replace("T"," ") # T는 큰 쓸모가 없으니 제거합니다
df['timestamp'] = df['timestamp'].str.split("+").str[0] # 한국 표준시 때문에 +09:00 이 붙는데 그 부분은 필요 없으니 제거해줍니다. (+를 기준으로 앞 부분만 남긴다는 뜻)
Python
복사
그러면 비로소 timestamp가 pandas의 datetime 규격에 맞아 떨어지게 되고, type 변경 메소드를 사용할 수 있습니다.
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['date_datetime'] = df[['date']].applymap(str).applymap(lambda s: "{}-{}-{}".format(s[0:4], s[4:6],s[6:]))
Python
복사
생각해보면, 밤 1시 쯤 자기 전 완료한 재활 운동은 다음날 수행한 데이터로 보기 보단,
그 전날 수행한 기록으로 보는 것이 합리적으로 보입니다.
이를 전처리 할 때는 주로 if: timestamp의 시간이 새벽 5시보다 낮을 경우 → date를 하루 전으로 표기 등의 전처리를 밟지만 STREAKS은 새벽 n시 이전 수행은 날짜를 전날로 보겠다 라는 필터 기능이 있었습니다.
만약 위 같은 상황에서 어플 자체 옵션을 걸지 않았다면 다음과 같은 전처리 코드가 필요했을 것입니다.
df.loc[df['timestamp'].dt.hour < 5, 'date_datetime'] = df.loc[df['timestamp'].dt.hour < 5, 'date_datetime'] - timedelta(days=1) # timedelta를 통해 하루를 빼주는 것입니다!
Python
복사
재활 운동 데이터를 시각화 해보자 
주의!
이번 시각화를 위해 많은 양의 datetime, timedelta 데이터 타입 전처리 과정이 소개됩니다
구현해 보고 싶은 것
1.
재활 운동 한 셋트 (5회 운동) 당 평균적으로 걸린 시간
2.
재활 운동을 수행한 시간대의 분포 (밤 늦게 했나? 아침에 일어나고 했나?)
짧은 발목 운동을 셋트 당 5회씩 반복한다. 과연 5번을 모두 하면 평균 몇 분 정도 소요될까? 
일단 데이터가 completed_manually_partial 로 1/5 의 운동을 할 때마다 체크가 됩니다.
이를 통해, n회 반복했는지를 카운트 할 수 있겠죠?
# 사전 처리 사항 : STREAK 앱에서 정체 불명의 'missed_auto' 데이터가 쌓여있다. 별 의미 없어보이니 이 행은 지우자.
df = df.loc[~(df['type'] == 'missed_auto')]
# 카운트를 세기 위한 count 데이터를 구축해보자.
df['count'] = 0 # 일단 0을 채운 공갈 칼럼 하나를 만듦
Python
복사
운동을 마치고 다음날 새로운 운동 세트를 시작했다는 걸 어떻게 구분 지을 수 있을까요? (카운트 리셋할 목적)
말 그대로, '다음날'이 되었는지를 검출하면 됩니다.
datetime을 통해 날짜가 기록된 데이터는 아래 shift 명령어로 "날짜가 변경되었음 (=다음날이 됨)"을 쉽게 검출할 수 있으니 참고하시면 됩니다.
# shift 명령어 : 시리즈의 한칸 앞(shift -1) 혹은 뒤 (shift +1) 값과 비교할 때 쓴다.
# 코드 해석 : 바로 뒤 행의 날짜가 지금 행과 다르면, 새로운 날짜가 되었으므로 카운트를 1로 친다. 왜? 새로운 운동이 시작되었다는 뜻이니까.
df.loc[df['date_datetime'] != df['date_datetime'].shift(+1), 'count'] = 1
Python
복사
새로운 카운트가 시작되었다면, 다음 날짜가 올 때까지는 카운트를 하나씩 올려봅니다.
for i in range(df.shape[0]):
if df.iloc[i, -1] == 0: # -1 인덱스는 방금 생성한 count 칼럼 데이터를 의미함.
df.iloc[i,-1] = df.iloc[i - 1,-1] + 1 # 앞선 카운트에 하나를 더 해서 +1을 만듦
Python
복사
3월 6일에 시작한 운동은 1,2로 카운트 되다가 7일로 바뀌니 새롭게 카운트 되고 있음이 확인된다.
이제 세트를 종료하는데 걸리는 평균 시간을 계산해야하므로 duration 데이터를 계산해봅시다. 
어떻게 계산할 것인가?
카운트가 새로 시작되는 1과 종료되는 5 간의 duration을 구해보면 되겠죠?
df['duration'] = 0 # 마찬가지로 공갈 칼럼으로 시작한다
for i in range(df.shape[0]):
if df.iloc[i,-2] == 5: # duration 공갈 칼럼이 새로 생겼으므로 count 칼럼은 -2 인덱스이다. 카운트가 5로 꽉 차면 운동이 종료되므로 5인지를 확인하는 코드를 넣었다.
count_five_index = i
for j in range(1,6):
if df.iloc[count_five_index - j, -2] == 1:
df.iloc[i, -1] = df.iloc[i,-4] - df.iloc[count_five_index - j, -4]
Python
복사
카운트가 5인 행의 duration 에 duration 데이터가 쌓였다!
모아서 보면 이상한 값도 있습니다.
6시간 동안 운동을 했을리 없고, 아마 5회 세트를 다 채우지 못해 이상한 값이 구해진 것이라 추측됩니다.
이런 녀석은 인간지능으로 걸러줍시다... 
(* 7행, 20행, 420행, 475행 값이 나옴)
명심해라. 손은 코딩보다 빠르다.
평균 duration은 아래 코드로 구할 수 있었습니다.
df.loc[(df['duration'] != 0) & ~(df.index.isin([7,20, 420, 475])), 'duration'].sum() / len(df.loc[(df['duration'] != 0) & ~(df.index.isin([7,20, 420, 475])), 'duration'])
Python
복사
.mean() 메소드 등을 사용하면 간단히 구해질 것 같긴 한데 timedelta 데이터가 꼬여서 쉽지 않아서,
그냥 .sum()을 한 뒤 데이터 수로 나눠주었습니다 (...ㅎㅎ)
5세트 운동하는데 평균 8분 42초!
하루 두 세트로 수행한 재활 운동. 주로 몇시 쯤에 했을까? 
그래프로 한 눈에 파악해봅시다!
complete 한 시점의 시각을 y축에 쏘고, x축엔 처음 한 날부터 끝낸 날까지를 쭉 펼쳐보면 될 것 같죠?
df['hour'] = df['timestamp'].dt.hour # 시각만 따로 추출하기. 분 단위는 생략한다.
Python
복사
import matplotlib.pyplot as plt
plt.figure(figsize=(10,5)) # 그래프 사이즈 조절.
plt.scatter(df['timestamp'], df['hour']) # x축은 날짜를, y축은 수행 시각을.
plt.xlabel('날짜')
plt.ylabel('수행 시각')
plt.show()
Python
복사
결과물을 보니, 자정 넘어서 한 운동이 아래 부분에 찍혀서 와닿지 않습니다.
늦은 시각에 운동하면 그 만큼 위에 찍혀야하는 거 아냐? 
그러면, 새벽 1시는 25시로 치자!
# 내맘대로 01:00 AM은 25:00으로 바꿔주는 easy_hour 로 변환해준다.
def easy_hour_maker(hour):
if hour < 5:
return hour + 24
else:
return hour
df['easy_hour'] = df['hour'].apply(easy_hour_maker)
# 그래프 다시 그리기
plt.scatter(df['timestamp'], df['easy_hour'])
plt.xlabel('날짜')
plt.ylabel('수행 시각')
plt.show()
Python
복사
처음엔 아침에도 했다가 점점 자정 무렵에 재활 운동을 했다는 데이터가 표현되었다.
저는 5개월 동안 아킬레스건 재활 운동을 5세트씩 2회 했습니다.
따라서 STREAKS은 아킬레스건2.csv 파일까지 제공하였는데, 동일하게 전처리하여 시각화해보면 됩니다.
데이터 꼴이 같으니 당연히 전처리, 수행 코드도 같다. 돌려보니 평균 수행 시간은 16분대로 증가!
시각화 결과물 역시 다음과 같았습니다.
그럼 아킬레스건 1번과 2번 운동을 동시에 시각화한다면??
*1번 운동은 동그라미, 2번 운동은 x로 표기
# 그래프 동시에 그리기
fig, ax = plt.subplots(figsize=(10,5))
ax.scatter(df['timestamp'], df['easy_hour'], marker='o', label = df['one_or_two'])
ax.scatter(df_2['timestamp'], df_2['easy_hour'], marker='x', label = df_2['one_or_two'])
plt.xlabel('날짜')
plt.ylabel('수행 시각')
plt.show()
Python
복사
사실상 재활 운동을 1차 이후 2차로 연달아 진행했기 때문에 시간적 격차가 크게 느껴지진 않네요!
STREAKS 어플을 통해 루틴을 기록하고, 이를 시각화하는 작업을 마무리했습니다.
2년이 지난 요즘에도 저는 STREAKS 앱을 애용합니다.
영양제 등을 잘 챙겨먹어야 하기 때문에 꼼꼼하게 기록하고 있습니다.
약물복용...