범고래닷컴!

로그인하면, 더 다양하게 사이트를 즐길 수 있습니다!

☹️ 지금까지 무슨 삶을 살아오신 거죠?

Sleepwalking은 지난 2년 간의 수면 데이터와 걷기 데이터로 바라본 제 삶 그 자체입니다.

대학생, 교환학생, 인턴을 거쳐 밤새는 병약한 대학생이 되기까지, 그 환경에 따라 변화하는 저의 생활 방식을 재미있게 살펴볼 수 있도록 기획했습니다.

☹️ What kind of life have you lived so far?

Sleepwalking is my life itself, as I've seen with sleep data and walking data over the past two years.

From being a college student, an exchange student, and an intern to being a sick college student all night, I planned to have fun looking at my changing lif estyle according to the environment.

Ideation

🤔 The part without any concern of getting data

Idea 1: My Sleeping Life

2년간의 수면 데이터로 본 내 생활

Background

아이폰에는 수면 시간을 기록해주는 기능이 있음

2년동안 수면 시간을 확인해본 결과, 재미있는 특성들을 발견함

이것들을 재미있게 시각화해서 수면 시간으로 살펴 본 내 생활 보고서를 만들면 어떨까?

Challenges

References

Sample Data

학기 중의 수면 데이터
인턴 기간 중의 수면 데이터
Idea 2: Genome 88

음악으로 음악가의 게놈 지도를 만들면 어떨까?

Background

Method

  1. 작곡가가 쓴 악보를 모두 모은다.
  2. 악보에 등장한 모든 음표를 취합해 그 빈도를 파악한다
  3. 장르나 곡의 특성 별로 모아, 막대 모양으로 시각화한다.
  4. 그렇게 한다면 유전자처럼 여러 막대를 만들 수 있지 않을까?

Challenges

Reference

Sample Data

Rejected !

이걸론 괜찮은 결과물이 절대 못나오겠군요☹☹☹

Idea 3: 이야기꽃 (talk flower..?)

단톡방은 이야기꽃밭(?)

Background

사람들은 메신저에 많은 단톡방을 가지고 있음

단톡방마다 그 성격도 다르고, 오가는 얘기도 다르고 주제도 다르다.

그럼에도 모두 이런저런 이야기가 오간다는 점에서 그 공통점이 있다

'이야기꽃을 피운다'라는 말에서 착안해, 내 카카오톡 단톡방을 분석해,각 단톡방을 꽃으로 표현해보면 어떨까라는 아이디어

Challenges

실제로 무슨 얘기만 해도 웃는 이상한 단톡방도 하나씩은 있습니다.

Reference

bp의 로고와 비슷한 형태로 꽃잎의 개수나 겹친 정도, 색깔을 통해 나타내보면 어떨까?
하지만 개인적으론 bp를 별로 좋아하지 않습니다
Idea 4: 🤔

Data and Sketch

☹️ Painful, endless iteration of data refining and idea sketch

1. How to get data [Programming]
  1. Apple 건강 앱으로부터 xml 형식의 건강 데이터를 추출
  2. Python 스크립트를 이용하여 해당 데이터를 csv로 변환
  3. 일별 수면 시간을 30분 단위로 묶어, 일별 수면 시간을 표로 생성
  4. 같은 방법으로, 건강 데이터로부터 걷기 기록을 모아 생성
2. Data visualization [Visualization]

주의 데이터 정제 결과이지, 프로젝트 초안이 아닙니다!
Warning This is visualization of processed data, not the draft for project!

#1 날짜 별 수면 시간 기록

#2 날짜 별 걸음 수 기록

낮에는 걷고, 밤에는 자는 기록..

3. Record of overnight torment [Idea Sketch]
4. Data visualization II [Visualization]

Output Images

Without y-axis noise
With y-axis noise

Data Snippet

# data-energy.csv
2021-10-09 0:00,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,98,620,600,0,194,440,341,0,0,0,0
2021-10-10 0:00,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,630,0,0,0,0,0,0,0,551,945,0,572,0,0,0,0,0,0,0,0
2021-10-11 0:00,651,0,0,0,0,0,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,759,723,0,0,0,0,258,0,64,0,804,0,801,0,0,0,0,0,0,0,0
2021-10-12 0:00,0,0,2278,5,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,117,0,0,60,60,0,0,0,0,0,9,0,0,15,1357,1174,306,1562,0,0,22,0,0,0,0,0,0
2021-10-13 0:00,2407,0,0,0,0,0,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,291,0,0,0,0,0,0,117,2548,834,0,1358,0,0,0,0,0,19,0,0
2021-10-14 0:00,0,0,1686,665,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,246,1233,961,0,0,2234,2187,0,0,0,0,0,0,0
2021-10-15 0:00,0,0,0,0,2233,2900,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109,0,0,0,288,0,8,1616,776,0,393,452,675,528,0,0,0,0,0,0,8,0,0
2021-10-16 0:00,0,979,1334,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,242,332,463,2931,364,0,0,0,0,0,491,1470,0,0,0,0,455,1881,42,42,0
2021-10-17 0:00,0,0,0,1939,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1961,64,16,938,385,244,550,0,0,0,0,0,0,0,0
...

Source Code

// sketch.js
function preload() {
  data = loadTable("result-energy.csv", "csv"); 
}

function setup() {
  createCanvas(600, 800);
  noLoop();
}

function draw() {
  //this makes sure there is content in the data
  if (data) { 
    //get the amount of rows in the CSV
    let numRows = data.getRowCount();
    let numCols = data.getColumnCount();
    
    print(numRows, numCols);
    
    for (let d = 0; d < numRows; d++) {
      for (let t = 1; t < numCols; t++) {
        // visualize the energy
        let energy = data.getNum(d, t);
        let length = map(energy, 0, 3000, 0, 30);
        
        // Change this random value for more randomness
        let offset = random(-4, 4);
        
        // Circle version
        // noStroke();
        // fill(231, 95, 65, 255 * 0.5);
        // circle(25 + 10 * t, 50 + 40 * Math.floor(d / 7) + offset, length);
        
        // Square version
        fill(231, 95, 65, 255 * 0.2);
        stroke(231, 95, 65, 255 * 0.8);
        let x = 25 + 10 * t;
        let y = 50 + 40 * Math.floor(d / 7) + offset - length / 2;
        square(x, y, length);
      }
    }
  }  
}

Reflection

Introduction

My initial sketch has two ideas of integrating static data(sleeping) with dynamic data(walking).

In this assignment, I tried to implement dynamic data using extracted csv data.

Color

I choose the red-orange color with transparency.

Since walking implies - daytime, sun, active, blood(?), fire - these color hue will suit.

In addition, I'm planning to use blue-navy color for static data.

Shape

Circular shape and square shape was introduced due to consistency between static data.

Wave + Circular

For circular shape, it will be placed after normal (or sine) wave which represents static data.

Since both are consisted of soft curves, it will fit well.

Square Hue + Square Size

For square shape, it will be placed after sequence of square with different color
which represents static data.

Since dynamic data is represented as size of square instead of color,
comparing difference between change in color and change in size would be effective.

Noise

To deliver image of being dynamic more effectively,
I introduced way to 'scatter' y-axis value of each square.
This would make each element fell as if they are alive and more dynamic.

5. Merge two data into one [Idea Sketch]

So, let's start that “engineers' stuff”☹

6. How to get data II [Programming]

다 때려부시고 처음부터 데이터 예쁘게 수집하기 프로젝트🥲

Do you know that p5.js rejected my dataset, just because the size of dataset was too big?

왜 다 때려부셨나

이전에 1장에서 XML 형식의 Apple 건강 데이터를 성공적으로 추출하는 데 성공했으나, 데이터에 일부 문제점이 있었습니다.

  1. 수동 작업이 불필요하게 많았습니다. 이후 작업에 필요한 데이터의 재현이 어렵습니다.
  2. 수면 데이터와 걷기 데이터를 각각 시각화하기는 편리했으나, 두 데이터를 합치기 불편합니다.
  3. 데이터를 얻어내기 위해 작성한 코드가 읽기 불편하고 어렵습니다.

이를 해결하기 위해 코드 수정 및 데이터를 추출하는 과정을 단순화했고, 그 결과는 다음과 같습니다.

  1. 일자 별로 48개의 블록으로 표현한 csv파일 대신에, 일자 구분 없이 30분 단위의 블록으로 구성된 텍스트 파일 내보내기를 만들었습니다.
    이렇게 구성된 수면 데이터와 걷기 데이터는 합치기에 훨씬 간단합니다.
    before = [[0, 0, 0, ... , 0], [0, 0, 0, ... , 0], ... , [0, 0, 0, ... , 0]]
    after = [0, 0, 0, 0, 0, ... 0, 0]
  2. 걷기 정보 데이터에 수동으로 삽입되던 지역 정보를 자동화했습니다.
    수면 정보와 달리 걷기 데이터에는 측정 지역 정보가 없어, 시차 보정을 직접 해야 합니다.
    이는 데이터의 재현성을 낮추는 문제가 있었습니다.
    이제 해당 지역 정보 데이터를 담은 csv를 만들어 자동으로 지역 정보를 불러 옵니다.
    # data-region.csv
    2022-07-11 00:00:00 +0900,Asia/Seoul
    2023-08-27 09:15:41 +0900,Asia/Riyadh
    2023-08-27 16:40:58 +0900,America/New_York
    2023-11-22 20:12:10 +0900,America/Toronto
    2023-11-26 23:16:18 +0900,America/New_York
    2023-12-24 22:13:39 +0900,Asia/Seoul
    2024-07-01 10:34:17 +0900,Asia/Dubai
    2024-07-03 02:10:24 +0900,Europe/Rome
    2024-07-05 17:05:53 +0900,Europe/Paris
    2024-07-20 10:09:08 +0900,Asia/Seoul
    2024-10-23 12:26:58 +0900,Australia/Sydney
    2024-10-29 16:32:12 +0900,Asia/Seoul

예쁘게 데이터를 모으기🥰

데이터를 정제해서 모으는 전체 프로세스는 다음과 같습니다.

수면 데이터
  1. xml 파일로부터 HKCategoryTypeIdentifierSleepAnalysis 태그를 가진 요소를 모두 취합
  2. 요소로부터 시작 시간, 종료 시간, 지역 정보를 가져와 data-sleep.csv로 저장
  3. 2022년 7월 11일부터 2024년 11월 26일까지, 30분 단위의 블록 리스트를 생성.
  4. data-sleep.csv의 각 열을 읽어, 시작 시간부터 종료 시간까지 해당하는 블록에 값 저장.→ 30분 단위의 블록 각각에 대해 해당 수면 시간이 차지하는 만큼의 시간(분) 값을 더함.→ (예) 11시 20분부터 12시 40분까지인 경우, 각 블록은 10, 30, 30, 10으로 채워짐.
  5. 블록 리스트를 result-sleep.txt로 저장
    # result-sleep.txt
    0
    0
    30
    30
    ...
    30
  6. 블록 리스트를 일별로 48개 단위로 나누어, result-sleep.csv로 저장
    # result-sleep.csv
    2022-07-11,0,0,0,0,0,0,30,30,30,30,30,30,30,30,30,30,30,30,30,30,0,0,0,...
    2022-07-12,0,0,0,0,0,0,0,11,30,30,30,30,30,30,30,30,30,30,30,30,28,0,0,0,...
    2022-07-13,0,0,0,0,0,0,0,0,0,10,30,30,30,30,30,30,30,30,30,30,0,0,0,0,0,0,...
    2022-07-14,0,0,0,0,0,0,0,24,30,30,30,30,30,30,30,30,30,30,30,30,9,0,0,0,0,...
걷기 데이터
  1. xml 파일로부터 HKQuantityTypeIdentifierStepCount 태그를 가진 요소를 모두 취합
  2. 요소로부터 시작 시간, 종료 시간, 걸음 수를 가져옴
  3. 시작 시간과 data-region.csv를 이용하여 지역 정보를 가져옴
  4. 가져온 정보를 data-walk.csv에 저장
  5. data-walk.csv의 각 열을 읽어, 시작 시간부터 종료 시간까지 해당하는 블록에 값 저장.
    → 30분 단위의 블록 각각에 대해, 블록 당 평균 걸음수만큼을 합산.
    → (예) 11시 20분부터 12시 40분까지 40걸음을 걸은 경우, 블록은 10, 10, 10, 10으로 채워짐.
  6. 블록 리스트를 result-walk.txt로 저장
    # result-walk.txt
    559
    823
    ...
    8
    605
    1745
  7. 블록 리스트를 일별로 48개 단위로 나누어, result-walk.csv로 저장
    # result-walk.csv
    2022-07-11,0,180,479,290,0,8,0,559,823,1494,344,694,303,8,605,1745,763,...
    2022-07-12,1943,0,1366,0,0,947,457,0,0,0,0,72,438,153,0,107,527,996,1628,...
    2022-07-13,0,1563,0,502,905,0,1346,382,0,0,1184,0,0,0,93,92,0,0,95,1336,...
    2022-07-14,0,0,597,92,0,0,46,0,0,1100,701,447,2548,1077,262,0,120,0,0,0,0,...
데이터 합치기
  1. result-sleep.txtresult-walk.txt로부터 수면 데이터 리스트를 생성
    sleep_list = [0, 0, 0, 0, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, ...]
    walk_list = [0, 0, 0, 0, 0, 180, 479, 290, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...]
  2. 오후 10시를 기준으로, 다음날 오후 10시까지 데이터를 모두 수집. 이후 수면 시간이 나타날 때까지 데이터를 추가.
    오후 10시가 기준인 이유는 모든 수면은 오후 10시 이후에 이루어졌기 때문.
    또한 데이터를 추가함으로써 오후 10시 ~ (다음 날 수면 직전)의 데이터를 모을 수 있음.(x, y)는 해당 블록동안 x분 수면, y 걸음을 걸었음을 의미.
    sleepwalk_list[0] = [(0, 0), (0, 180), (0, 479), (0, 290), (0, 0), ...]
    sleepwalk_list[1] = [(0, 410), (0, 410), (0, 0), (0, 0), (0, 1943), ...]
    sleepwalk_list[2] = [(0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 1563), ...]
    sleepwalk_list[3] = [(0, 1410), (0, 771), (0, 0), (0, 0), (0, 0), ...]
    sleepwalk_list[4] = [(0, 0), (0, 0), (0, 0), (0, 0), (0, 0), (0, 0), ...]
  3. 이렇게 만들어진 데이터를 result-sleepwalk.json의 형태로 저장.
    // result-sleepwalk.json
    {
    "2022-07-11": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 180], ...],
    "2022-07-12": [[0, 410], [0, 410], [0, 0], [0, 0], [0, 1943], ...], 
    "2022-07-13": [[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 1563], ...], 
    "2022-07-14": [[0, 1410], [0, 771], [0, 0], [0, 0], [0, 0], [0, 0], ...],
    ...
    }

소스 코드

혹시 다른 분들께 도움이 되진 않을까 싶어, 작성한 코드를 공유합니다.

참고사항

  1. Jupyter Notebook 형식의 python으로 작성되었습니다.
  2. 특정 데이터를 정제하기 위한 용도이기에, 도움이 되지 않을 수도 있습니다.
  3. 데이터셋은 제공하지 않습니다만, xml을 제외한 데이터 형식은 위에서 설명드렸습니다.
  4. xml 데이터는 apple 건강으로부터 내려받을 수 있습니다.
    건강 → 프로필 클릭 → 하단에 데이터 내보내기 선택 → 압축파일 다운로드의 방법으로데이터를 얻을 수 있으니 시간이 되신다면 직접 해 보시는 것도 추천드립니다.
    다만, 저처럼 지역이 여러 번 왔다갔다 한 경우, 시차 보정에 애를 먹을 수도 있습니다☹

소스 코드

f-sleep.py
# %% [markdown]
# Let's collect sleep data from xml data!

# %%
import xml.etree.ElementTree as ET
import csv

tree = ET.parse("../export.xml")
root = tree.getroot()

with open("data-sleep.csv", "w", encoding="utf-8-sig", newline="") as fp:
writer = csv.writer(fp)

for child in root:
  if (
      child.tag == "Record"
      and child.attrib["type"] == "HKCategoryTypeIdentifierSleepAnalysis"
  ):
      start, end = child.attrib["startDate"], child.attrib["endDate"]
      location = child[0].attrib["value"]

      # Filter date
      if start < "2022-07-11 00:00:00 +0900":
          continue

      row = [start, end, location]

      print(row)
      writer.writerow(row)

# %% [markdown]
# Convert extracted csv into flatten list.

# %%
from datetime import datetime
from zoneinfo import ZoneInfo

import csv

TIME_FORMAT = "%Y-%m-%d %H:%M:%S"

# Initialize time
# Data is collected from 2022-07-11.
init_time = datetime(2022, 7, 11)
curr_time = datetime.today()

# Initialize sleep list
sleep_list = [0 for _ in range((curr_time - init_time).days)] * 48

with open("data-sleep.csv", "r", encoding="utf-8-sig") as fp:
reader = csv.reader(fp)

for start, end, region in reader:
  start_time = datetime.strptime(start[:-6], TIME_FORMAT)
  end_time = datetime.strptime(end[:-6], TIME_FORMAT)

  # Get timezone info
  try:
      tz_curr = ZoneInfo(region)
  except:
      continue

  start_time = start_time.astimezone(tz_curr).replace(tzinfo=None)
  end_time = end_time.astimezone(tz_curr).replace(tzinfo=None)

  start_delta = start_time - init_time
  end_delta = end_time - init_time

  start_index = (start_delta.days * 48) + (start_delta.seconds // 1800)
  end_index = (end_delta.days * 48) + (end_delta.seconds // 1800)

  # Skip counting if time is before time_init
  if start_index < 0:
      continue

  start_value = (1800 - (start_delta.seconds % 1800)) // 60
  end_value = (end_delta.seconds % 1800) // 60

  for index in range(start_index, end_index + 1):
      if index == start_index:
          sleep_list[index] = start_value
      elif index == end_index:
          sleep_list[index] = end_value
      else:
          sleep_list[index] = 30

print(sleep_list)

# %% [markdown]
# Export sleep list into text

# %%
with open("result-sleep.txt", "w", encoding="utf-8") as fp:
for sleep in sleep_list:
  fp.write(f"{sleep}\n")

# %% [markdown]
# Export sleep list into csv (deprecated maybe...)

# %%
from datetime import datetime, timedelta

init_time = datetime(2022, 7, 11)

with open("result-sleep.csv", "w", encoding="utf-8-sig", newline="") as fp:
writer = csv.writer(fp)

for day in range(len(sleep_list) // 48):
  index = day * 48
  date = datetime.strftime(init_time + timedelta(day), "%Y-%m-%d")
  row = [date, *sleep_list[index : index + 48]]

  writer.writerow(row)
f-walk.py
# %% [markdown]
# There is no timezone data in walk data.
# Let's get data from hand-made csv file!

# %%
import csv


def get_region(date: str):
  with open("data-region.csv", "r", encoding="utf-8-sig") as fp:
      reader = csv.reader(fp)
      result = ""

      for bound, region in reader:
          if date >= bound:
              result = region
          else:
              break

  return result

# %% [markdown]
# Let's collect sleep data from xml data!

# %%
import xml.etree.ElementTree as ET
import csv

tree = ET.parse("../export.xml")
root = tree.getroot()

with open("data-walk.csv", "w", encoding="utf-8-sig", newline="") as fp:
  writer = csv.writer(fp)
  for child in root:
      if (
          child.tag == "Record"
          and child.attrib["type"] == "HKQuantityTypeIdentifierStepCount"
      ):
          start = child.attrib["startDate"]
          end = child.attrib["endDate"]
          steps = child.attrib["value"]
          region = get_region(start)

          # Filter date
          if start < "2022-07-11 00:00:00 +0900":
              continue

          row = [start, end, steps, region]

          print(row)
          writer.writerow(row)

# %% [markdown]
# Convert extracted csv into flatten list.

# %%
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import csv

TIME_FORMAT = "%Y-%m-%d %H:%M:%S"

# Initialize time and result list
init_time = datetime(2022, 7, 11)
curr_time = datetime.today()

# Initialize walk list
walk_list = [0 for _ in range((curr_time - init_time).days)] * 48

with open("data-walk.csv", "r", encoding="utf-8-sig") as fp:
  reader = csv.reader(fp)

  for start, end, steps, region in reader:
      start_time = datetime.strptime(start[:-6], TIME_FORMAT)
      end_time = datetime.strptime(end[:-6], TIME_FORMAT)

      # Get timezone info
      try:
          tz_curr = ZoneInfo(region)
      except:
          continue
      
      start_time = start_time.astimezone(tz_curr).replace(tzinfo=None)
      end_time = end_time.astimezone(tz_curr).replace(tzinfo=None)

      start_delta = start_time - init_time
      end_delta = end_time - init_time

      start_index = (start_delta.days * 48) + (start_delta.seconds // 1800)
      end_index = (end_delta.days * 48) + (end_delta.seconds // 1800)

      start_value = (1800 - (start_delta.seconds % 1800)) // 60
      end_value = (end_delta.seconds % 1800) // 60

      steps = int(steps) // (end_index - start_index + 1)

      for index in range(start_index, end_index + 1):
          walk_list[index] += steps

print(walk_list)

# %% [markdown]
# Export sleep list into text

# %%
with open("result-walk.txt", "w", encoding="utf-8") as fp:
  for walk in walk_list:
      fp.write(f"{walk}\n")

# %% [markdown]
# Export sleep list into csv (deprecated maybe...)

# %%
from datetime import datetime, timedelta

with open("result-walk.csv", "w", encoding="utf-8-sig", newline="") as fp:
  writer = csv.writer(fp)

  for day in range(len(walk_list) // 48):
      index = day * 48
      date = datetime.strftime(init_time + timedelta(day), "%Y-%m-%d")
      row = [date, *walk_list[index : index + 48]]

      writer.writerow(row)
f-merge.py
# %% [markdown]
# ### Merge sleep data with walking data
# 
# Ah, engineering thing!

# %% [markdown]
# Collect sleep and walk data from text result.

# %%
# Start data from 22:00
sleep_list = [0, 0, 0, 0]
walk_list = [0, 0, 0, 0]

with open("result-sleep.txt", "r", encoding="utf-8") as fp:
  sleep_list += [int(line) for line in fp.readlines()]

with open("result-walk.txt", "r", encoding="utf-8") as fp:
  walk_list += [int(line) for line in fp.readlines()]

print(sleep_list)
print(len(sleep_list))
print(walk_list)
print(len(walk_list))

# %% [markdown]
# Zip two data into tuple and apply dynamic daylength.
# 
# Q. Is making element as tuple is really a good idea?

# %%
sleepwalk_data = list(zip(sleep_list, walk_list))
sleepwalk_list = []

for day in range(len(sleepwalk_data) // 48):
  index = day * 48
  buffer = sleepwalk_data[index : index + 48]

  # Append data until the sleeping time occurs.
  # It is guaranteed that `index + 48` is not a sleeping time.
  for sleep, walk in sleepwalk_data[index + 48 : index + 96]:
      if sleep > 0:
          break

      # Add data to buffer
      buffer.append((sleep, walk))

  # Add buffer to result list
  sleepwalk_list.append(buffer)

_ = [print(sleepwalk) for sleepwalk in sleepwalk_list]

# %% [markdown]
# Export sleep list into json.

# %%
from datetime import datetime, timedelta
import json

# Set initial time
init_time = datetime(2022, 7, 11)

# Convert sleepwalk list into json object
json_obj = {}

for day, sleepwalk in enumerate(sleepwalk_list):
  date = datetime.strftime(init_time + timedelta(day), "%Y-%m-%d")
  json_obj[date] = [[sleep, walk] for sleep, walk in sleepwalk]

# Print result log
for k, v in json_obj.items():
  print(k, v)

# Save json object as json file
with open("result-merge.json", "w") as fp:
  json.dump(json_obj, fp)


7. Data visualization III [Visualization]

이히히히히히 된다 된다 된다 히히히🤪

이미지! (주의: 몹시 깁니다😮)
Source Code
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.6.0/p5.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/addons/p5.sound.min.js"></script>
  <script src="https://unpkg.com/p5.js-svg@1.5.0"></script>
  <link rel="stylesheet" type="text/css" href="style.css">
  <meta charset="utf-8" />

</head>
<body>
  <main>
  </main>
  <script src="sketch.js"></script>
</body>
</html>
// sketch.js
function preload() {
  dataJSON = loadJSON("result-merge.json");
}

function setup() {
  createCanvas(1600, 5300, SVG);
  noLoop();
}

function draw() {
  //this makes sure there is content in the data
  if (dataJSON) {
      let data = dataJSON.data;
      print(data.length)
      for (let i = 0; i < data.length; i++) {
          let datum = data[i];
          let date = datum.date;
          let sleepwalk = datum.sleepwalk;

          // Add label
          if (i % 7 == 0) {
              fill(0, 0, 0);
              noStroke();
              text(date, 10, 40 * Math.floor(i / 7) + 55);
          }

          for (let j = 0; j < sleepwalk.length; j++) {
              // Unpack sleep and walk value
              let sleep = sleepwalk[j][0];
              let walk = sleepwalk[j][1];

              // Visualize sleep
              let sleep_size = 5;
              let sleep_alpha = map(sleep, 0, 30, 0, 255 * 0.14)

              fill(121, 166, 205, sleep_alpha);
              stroke(121, 166, 205, sleep_alpha * 4);
              let x_sleep = 80 + 10 * j;
              let y_sleep = 50 + 40 * Math.floor(i / 7) - sleep_size / 2;
              square(x_sleep, y_sleep, sleep_size);

              // Visualize walk
              let walk_size = map(walk, 0, 3000, 0, 30);
              let walk_offset = random(-4, 4);
            
              if (walk_size) {
                  fill(231, 95, 65, 255 * 0.14);
                  stroke(231, 95, 65, 255 * 0.56);
              } else {
                  noFill();
                  noStroke();
              }
              
              let x_walk = 80 + 10 * j;
              let y_walk = 50 + 40 * Math.floor(i / 7) + walk_offset - walk_size / 2;
              square(x_walk, y_walk, walk_size);
          }
      }
  }
}
관전 포인트👀
일반적인 학기 중
자는 시간이 조금 늦다. 수업 들어야 해서 걸음수가 제법 된다.
미국 교환학생
자는 시간이 이르지만 편차가 있다. 볼 게 많아서 엄청나게 걸어 다닌다.
인턴 기간
자는 시간이 매우 이르고 규칙적이다. 점심 시간에만 걷고 다른 때에는 앉아 있다.
유럽 여행
엄청나게 걸어 다닌다. 중간에 핸드폰을 소매치기 당해서 기록이 없다 (젠장)
이번 학기
수면 편차가 크고, 수면량이 부족해 데이터가 옅고, 걷기 데이터와 겹친다.
수업도 맨날 빠지고 걷지도 않아서 일주일 치 데이터인데도 사각형이 작고 듬성듬성하다.
나는 이번 학기에 어떤 삶을 살고 있는 걸까?😢(아래에는 보이지 않지만, 밤을 샌 기록도 상당히 많다)
사실 지금도 밤을 새고 있다
무엇을 고려해야 하느냐?
  • 주 단위로 묶었는데도 데이터가 너무 많다 (125주, 총 877일).데이터를 줄이거나, 관전 포인트에서 본 것처럼 기간 별로 카테고리화해야 할 것 같다.
  • 범례도 디자인틱(??)하게 줘야 한다. (밤을 새니 어휘가 생각이 나지 않습니다)
  • 눈에 보여지는 데이터가 어떤 정보를 나타내는지 주석을 달아야 한다.
  • 이제 레이아웃을 고민해야 한다 (제일 어려운 것)
  • 최적화: 지금 저 그래픽을 그대로 내보내면 오브젝트가 너무 많다.
    보이지는 않지만, 여전히 그려진 오브젝트가 많고, 겹쳐 있는 녀석들도 많아서 그렇다.(특히 수면 데이터 파트..컴퓨터가 울부짖습니다…)
    데이터셋을 일 단위에서 주 단위로 묶고, 보이지 않는 오브젝트는 그리지 않게 최적화.
8. Optimize SVG [Programming]

Merge it! Remove it! Simplify it!
It will make your SVG much smaller but make your code much bigger☹☹

또 뭐가 문제였길래

6, 7장에서 일단 성공적으로 데이터를 시각화하는 데는 성공했습니다.

하지만 여전히 할 일은 많았는데, 만들어진 SVG 데이터가 너무 커서 일러스트레이터에서 사용하기에 너무 불편하다는 문제가 있었습니다.

무려 16MB를 넘는 육중한 사이즈에 컴퓨터는 울부짖고 방은 히터를 안 때도 따뜻합니다.

이를 해결하기 위해 일부 최적화를 진행했습니다.

코드의 변경 사항

위의 피나는(?) 노력 덕분에, 16MB가 넘던 SVG 데이터는 이제 3.6MB 수준으로 감소했습니다.
컴퓨터도 이제는 덜 화난 것 같군요!

소스 코드!

sketch.js
// sketch.js
function preload() {
dataJSON = loadJSON("assets/result-merge.json");
font = loadFont("assets/Pretendard-Bold.otf");
}

function setup() {
createCanvas(1600, 5300, SVG);  // Create canvas
noStroke();                     // We don't use stroke
noLoop();                       // Iterate only once
}

function draw() {
//this makes sure there is content in the data
if (dataJSON) {
let data = dataJSON.data;
let weekly_sleep = new Array(96).fill(0);

for (let i = 0; i < data.length; i++) {
let datum = data[i];
let date = datum.date;
let sleepwalk = datum.sleepwalk;

// Work to do for the first weekday
if (i % 7 == 0) {
  // Add title for row
  fill(0, 0, 0);
  textSize(10);
  textFont(font);
  text(date, 10, 40 * Math.floor(i / 7) + 55);

  // Reset weekly sleep array
  weekly_sleep = new Array(96).fill(0);
}

// Iterate blocks in daily sleepwalk data
for (let j = 0; j < sleepwalk.length; j++) {
  // Unpack sleep and walk value
  let sleep = sleepwalk[j][0];
  let walk = sleepwalk[j][1];

  // Append sleep data into weekly sleep
  weekly_sleep[j] += sleep;

  // Calculate pivot coordinate
  let x = 80 + 10 * j;
  let y = 50 + 40 * Math.floor(i / 7);

  // Visualize walk
  let walk_size = map(walk, 0, 3000, 0, 30);
  let walk_offset = random(-4, 4);

  if (walk_size) {
    let x_walk = x - walk_size / 2;
    let y_walk = y + walk_offset - walk_size / 2;

    fill(220, 70, 20, 255 * 0.25);
    square(x_walk, y_walk, walk_size);
  }
}

// Work to do for the last weekday
if (i % 7 == 6) {
  // Draw weekly sleep data
  for (let j = 0; j < 96; j++) {
    // Unpack sleep value from weekly sleep
    let sleep = weekly_sleep[j];

    // Calculate pivot coordinate
    let x = 80 + 10 * j;
    let y = 50 + 40 * Math.floor(i / 7);

    // Visualize sleep
    let sleep_size = 8;
    let sleep_alpha = map(sleep, 0, 210, 0, 255)

    if (sleep_alpha) {
      let x_sleep = x - sleep_size / 2;
      let y_sleep = y - sleep_size / 2;

      fill(25, 50, 75, sleep_alpha);
      square(x_sleep, y_sleep, sleep_size);
    }
  }
}
}
}
}

디자인의 변경 사항

디자인 상으로도 약간의 변화를 주었습니다.

디자인 미리보기!

전체 이미지 (왕 큼 주의)
훨씬 세련되어 보이지 않나요? 제발 그렇다고 해 주세요 엉엉
9. Concerning Layouts [Idea Sketch]

최대한 저 시각화를 아름답게, 그리고 사람들이 읽기 편하게 만들어야 해요🤔

구분선 사용하기!

구분선과 여백을 임시로 추가한 버전 (feat. 그림판😉)

구분선 없이 위의 데이터를 바라본다면, 사람들은 단순히 시간의 연속적인 흐름에 따른 변화만을 확인할 수 있을 것입니다.

하지만 구분선을 통해 연속적인 시간의 흐름을 그룹화할 수 있고,각 그룹 사이의 비교를 통해 보다 직관적이고 확실한 비교가 가능해지게 됩니다.

구분선을 어떻게 그어야 할까요?

  1. 가로선: 기간/장소에 따른 구분날짜에 따라 살펴 보면, 있었던 장소나 하고 있던 일이 모두 다른 것을 볼 수 있습니다.구분선을 통해 기간 별 시회적 지위(??)에 따른 구분, 그리고 그에 따른 수면/걷기 패턴을 관찰할 수 있을 것입니다.
  2. 세로선: 날짜 변경선(?)앞서 5장의 스케치에서 다루었듯이, 한 줄의 전체 시간은 모두 다릅니다.전날보다 잠을 늦게 잘수록 활동 시간이 늘어나니 그에 따라 줄의 시간도 늘어나지요.(밤을 새면 무려 24시간이 통짜로 추가되니, 한 줄의 최대 시간은 48시간입니다!)그러나 구분선 없이 언제가 시작이고 언제가 끝인지 확인하기 불편합니다.이를 해결하기 위해 세로선을 통해 시간 단위를 작성해주면 어떨까 생각했습니다.선을 길게 그어, 어디를 보던 이 사람이 몇시쯤 자고 몇시에 일어나서 언제까지 걸어다녔는지확인을 할 수가 있겠습니다.

압축!!!

위아래로 좀 누르고 비율을 바꾼 버전 (위아래 공간도 확보 !)

이전의 데이터는 위아래 여백이 너무 넓어서, 쓸데 없이 낭비되는 공간이 많았습니다.

줄 간격을 줄이고, 대신 너비를 넓혀 줄 오른쪽에 캡션이나 설명을 넣을 수 있도록 합니다.

이제 높이 대 너비 비도 3:1에서 2:1로 줄어들었습니다! 인쇄하기 훨씬 좋겠군요!

sketch.js
// sketch.js
function preload() {
dataJSON = loadJSON("assets/result-merge.json");
font = loadFont("assets/Pretendard-Bold.otf");
}

function setup() {
createCanvas(1300, 3900, SVG);  // Create canvas
noStroke();                     // We don't use stroke
noLoop();                       // Iterate only once
}

function draw() {
//this makes sure there is content in the data
if (dataJSON) {
let data = dataJSON.data;
let weekly_sleep = new Array(96).fill(0);

for (let i = 0; i < 630; i++) {  // 7 days * 90 weeks
let datum = data[i];
let date = datum.date;
let sleepwalk = datum.sleepwalk;

// Work to do for the first weekday
if (i % 7 == 0) {
  // Add title for row
  fill(0, 0, 0);
  textSize(10);
  textFont(font);
  text(date, 50, 205 + 30 * Math.floor(i / 7));

  // Reset weekly sleep array
  weekly_sleep = new Array(96).fill(0);
}

// Iterate blocks in daily sleepwalk data
for (let j = 0; j < sleepwalk.length; j++) {
  // Unpack sleep and walk value
  let sleep = sleepwalk[j][0];
  let walk = sleepwalk[j][1];

  // Append sleep data into weekly sleep
  weekly_sleep[j] += sleep;

  // Calculate pivot coordinate
  let x = 120 + 15 * j;
  let y = 200 + 30 * Math.floor(i / 7);

  // Visualize walk
  let walk_size = map(walk, 0, 3000, 0, 30);
  let walk_offset = random(-4, 4);

  if (walk_size) {
    let x_walk = x - walk_size / 2;
    let y_walk = y + walk_offset - walk_size / 2;

    fill(220, 70, 20, 255 * 0.25);
    square(x_walk, y_walk, walk_size);
  }
}

// Work to do for the last weekday
if (i % 7 == 6) {
  // Draw weekly sleep data
  for (let j = 0; j < 96; j++) {
    // Unpack sleep value from weekly sleep
    let sleep = weekly_sleep[j];

    // Calculate pivot coordinate
    let x = 120 + 15 * j;
    let y = 200 + 30 * Math.floor(i / 7);

    // Visualize sleep
    let sleep_size = 12;
    let sleep_alpha = map(sleep, 0, 210, 0, 255)

    if (sleep_alpha) {
      let x_sleep = x - sleep_size / 2;
      let y_sleep = y - sleep_size / 2;

      fill(25, 50, 75, sleep_alpha);
      square(x_sleep, y_sleep, sleep_size);
    }
  }
}
}
}
}
10. Color [Visualization]

Find the best color for project 🌈

초안의 이상한(?) 색깔을 멀쩡하게 바꾸려는 시도입니다.
다음과 같은 사항이 고려되었습니다.

  1. Sleepwalking의 주제에 맞게 밤의 색깔을 사용할 것
  2. 수면 데이터와 걷기 데이터 사이의 어느 정도 대비를 보일 것
  3. (중요) 보기에 예뻐 보일 것

수많은 레퍼런스 탐색과 끝없는 색깔 섞기를 반복한 끝에, 사용할 색을 정했습니다.
애착 형성(???)을 위해 색깔 별로 이름도 붙여 주었습니다.

Midnight Navy #06121F

차가운 도시의 밤 색깔

배경 색깔로 활용

Moon Yellow #F5E162

밝게 빛나는 달의 색깔

수면 데이터 색깔로 활용

Moonstone Jade #6BC8C3

구름에 비치는 달빛 색깔

걷기 데이터 색깔로 활용

Drafts

Draft I

Feedback

Draft II

Changes

충격적인 그리드 사진

Self-criticism

Feedbacks

Draft III

Changes

After Class

2024.12.23.

애플이 또

sleep-walking의 데이터는 아이폰이 사용자의 화면 사용 여부에 따라 사용자의 수면 패턴을 기록하는 기능이 있기에 모아질 수 있었습니다.

빌어먹을 애플은 iOS18 업데이트 이후로 이 기능을 삭제했고, 수면 데이터 수집을 위해서는 반드시 애플워치를 등록하여 차고 자야 하게 되었습니다.

저도 애플 사용자긴 하지만, 저는 애플이 참 밉습니다. Sleepwalking은 강제로 치료당했고, 이제는 walking만 남게 되었습니다.

업데이트는 9월 경이었는데, 제가 과제하느라 죽어가면서 업데이트를 질질 미뤄서정말 다행히도 11월 말까지 데이터가 모일 수 있었습니다.

애플이 제 프로젝트를 망칠 뻔했군요..


return to project list