소개
지역 전시 시설에서 개관 시간대를 공정하게 비교해야 합니다. 방문객 합계가 큰 이유는 단지 기록할 기회가 더 많았기 때문일 수도 있습니다. 먼저 관측 시간이 같은 두 시간대를 비교한 뒤, 계획된 개관 시간, 기록된 시간, 아직 알 수 없는 값을 구분해 보존하는 재사용 가능한 분석을 만듭니다.
이 실습은 Python 기초, pandas의 읽기·필터링·그룹화, Matplotlib 막대그래프를 알고 있다고 가정합니다. WebIDE에는 Python과 필요한 라이브러리가 준비되어 있습니다. 모든 집계 데이터는 합성이며, 한 행은 한 시간을 나타내고 개인의 신원을 나타내지 않습니다. 소요 시간은 읽기와 일반적인 실수 복구를 포함한 작성자의 추정치입니다. 원본 입력을 모두 보존하면서 실제 CSV와 PNG 파일을 만듭니다.
관측 시간이 같은 두 시간대 비교하기
이 단계에서는 첫 그래프를 그리기 전에 시간당 방문객 수를 손으로 계산합니다. WebIDE Tab 아래쪽 터미널을 사용하세요. 보이지 않으면 왼쪽 위 메뉴에서 Terminal → New Terminal을 선택합니다. 명령은 이 터미널에서 실행되며 파일은 왼쪽 Explorer에 나타납니다.
준비된 작업 폴더로 이동합니다. cd는 이 터미널의 작업 디렉터리만 바꾸며 다른 터미널에는 영향을 주지 않습니다. pwd는 현재 디렉터리를 표시합니다. 나중에 파일이 없다고 나오면 여기로 돌아와 ls로 이름 철자를 확인하세요.
cd /home/labex/project/opening-hours
pwd
/home/labex/project/opening-hours
cat은 텍스트 파일을 바꾸지 않고 표시합니다. 이 CSV에는 시간대마다 한 시간짜리 관측이 두 개씩 있습니다.
cat data/equal.csv
slot,visitors
morning,6
morning,10
evening,8
evening,12
아침에는 기록된 두 시간에 총 16명이므로 시간당 16 / 2 = 8명입니다. 저녁에는 두 시간에 20명이므로 20 / 2 = 10명입니다. 분모가 같으므로 합계와 시간당 수치 모두 저녁이 더 큽니다. 이 수치의 단위는 '기록된 한 시간당 방문객 수'입니다.
다음 짧은 스크립트를 만듭니다. 셸의 >는 지정한 파일을 덮어씁니다. <<'PY'는 히어 도큐먼트를 시작하며, PY만 있는 줄까지 텍스트를 그대로 저장합니다. 마지막 줄을 포함한 블록 전체를 붙여 넣으세요. 파일이 즉시 만들어지므로 편집기에서 저장할 필요가 없습니다. agg는 결과 열 이름을 지정하고 size는 관측 시간 수를 셉니다. index=False는 pandas 행 인덱스를 CSV에 쓰지 않는 옵션입니다. Agg를 사용하면 별도 창 없이 Matplotlib이 PNG를 저장합니다.
cat > equal.py <<'PY'
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
data = pd.read_csv('data/equal.csv')
summary = data.groupby('slot').agg(visitors=('visitors', 'sum'), hours=('visitors', 'size'))
summary['rate'] = summary['visitors'] / summary['hours']
summary.to_csv('results/equal-summary.csv')
summary['rate'].plot.bar(color=['#256b5d', '#b35828'], rot=0)
plt.ylabel('Visitors per recorded hour')
plt.title('Equal recording opportunities: two hours each')
plt.tight_layout()
plt.savefig('results/equal.png', dpi=150)
plt.close()
print(summary)
PY
python3 equal.py
visitors hours rate
slot
evening 20 2 10.0
morning 16 2 8.0
행은 시간대 이름순입니다. 출력의 앞쪽 공백은 조금 다를 수 있습니다. Explorer에서 opening-hours/results/equal.png를 열어 저장된 그래프를 보세요. 세로 막대 두 개는 기록된 한 시간당 10명과 8명을 보여 줍니다. 가로축 눈금에는 시간대 이름을 유지하고, 세로축 레이블은 Visitors per recorded hour로 합니다. 색상, 글꼴, 이미지 크기, 시간대 순서는 바꿔도 됩니다. PNG는 제공된 목표 이미지가 아니라 직접 만든 실제 결과입니다. 그래프는 결정적이어야 합니다. 입력이 같으면 현재 그리기 스크립트를 다시 실행했을 때 같은 이미지 픽셀이 나와야 합니다. 무작위 스타일과 현재 시각 표시는 피하세요. PNG 메타데이터는 달라도 됩니다. 색상, 글꼴, 크기는 자유롭게 선택할 수 있습니다.
흔한 파일 이름 실수는 Python이 파일이 없다는 오류를 내고 멈출 뿐, 작업을 초기화하지 않습니다. 다음 실패는 의도한 예시입니다.
python3 missing.py
can't open file과 No such file or directory가 표시되어야 합니다. 작업 폴더의 파일 목록을 보고 올바른 이름으로 다시 실행하면 복구됩니다.
ls
python3 equal.py
자가 확인: 두 시간대 모두 두 시간의 관측을 사용합니다. 앞으로 모든 데이터의 기록 기회가 같다고 가정한 것은 아닙니다.
계획된 모든 시간 보존하기
이 단계에서는 방문객 수를 해당 계획 시간에 연결합니다. /home/labex/project/opening-hours의 WebIDE 터미널에서 계속하세요. 계획의 각 행은 특정 날짜, 특정 시간대의 한 시간입니다. open=1은 개관 예정, open=0은 휴관을 뜻합니다. date와 slot의 쌍이 복합 키입니다. 어느 한 열만으로는 행을 식별할 수 없습니다. 날짜 형식은 YYYY-MM-DD이고 파일의 날짜는 모두 유효한 달력 날짜입니다.
cat data/plan.csv
cat data/counts.csv
방문객 수 파일은 의도적으로 다른 순서입니다. 0은 방문객이 없었다는 실제 관측입니다. 개관 예정 시간에 대응하는 방문객 수가 없으면 기록이 누락된 것이지 방문객이 0인 것이 아닙니다. 휴관 시간에는 방문객 수 행이 없습니다. 각 방문객 수는 개관 예정인 계획 행과 대응해야 합니다. 두 파일의 복합 키는 빠진 값 없이 고유해야 합니다. 잘못된 키를 숨기려고 중복을 제거하거나 방문객 수를 지어내지 마세요.
**왼쪽 병합(left merge)**은 왼쪽 표인 계획의 모든 행을 유지합니다. 행 위치가 아니라 키 두 열로 방문객 수를 찾습니다. 일치하는 행이 없으면 pandas 결측값이 들어가 NaN으로 표시됩니다. 병합 전에 키가 완전한지 확인하세요. pandas는 누락된 키끼리 연결할 수 있지만, 그것으로 실제 한 시간을 식별할 수는 없습니다. duplicated(keys)는 중복 키 쌍을 찾아내고 validate='one_to_one'은 중복 관계를 거부하도록 요구합니다.
스크립트의 .loc[condition, column]은 조건에 맞는 행에만 값을 대입합니다. .eq(0)과 .eq(1)은 각 값을 0 또는 1과 비교하고, &는 조건을 결합하며, notna()는 값이 있는지 빠졌는지 구분합니다. 먼저 휴관 마스크를 적용한 다음 개관하면서 기록도 있는 마스크를 적용하므로, 실제 0도 관측된 것으로 처리됩니다.
준비된 observe.py를 다음 완전한 스크립트로 바꿉니다. sys.argv[1]과 [2]는 스크립트 이름 뒤의 두 경로 인수를 읽습니다. Path와 /를 함께 사용해 파일 경로를 만듭니다. mkdir(parents=True, exist_ok=True)는 필요할 때 출력 디렉터리를 만들어 같은 프로그램으로 다른 입력 폴더도 분석할 수 있게 합니다. 두 입력 CSV는 읽기만 하고 변경하지 않습니다.
cat > observe.py <<'PY'
import sys
from pathlib import Path
import pandas as pd
source = Path(sys.argv[1])
output = Path(sys.argv[2])
plan = pd.read_csv(source / 'plan.csv')
counts = pd.read_csv(source / 'counts.csv')
keys = ['date', 'slot']
if plan[keys].isna().any().any() or counts[keys].isna().any().any():
raise ValueError('Every date and slot must be present')
if plan.duplicated(keys).any() or counts.duplicated(keys).any():
raise ValueError('Every date/slot key must be unique')
if not plan['open'].isin([0, 1]).all():
raise ValueError('open must be 0 or 1')
if counts['visitors'].isna().any() or (counts['visitors'] < 0).any():
raise ValueError('Recorded counts must be nonnegative')
known = counts.merge(plan, on=keys, how='left', validate='one_to_one')
if known['open'].isna().any() or (known['open'] != 1).any():
raise ValueError('Counts must match an open planned hour')
hours = plan.merge(counts, on=keys, how='left', validate='one_to_one')
hours['status'] = 'missing'
hours.loc[hours['open'].eq(0), 'status'] = 'closed'
hours.loc[hours['open'].eq(1) & hours['visitors'].notna(), 'status'] = 'observed'
hours['valid_hour'] = hours['status'].eq('observed').astype(int)
output.mkdir(parents=True, exist_ok=True)
hours.sort_values(keys).to_csv(output / 'hours.csv', index=False)
print(hours[['date', 'slot', 'visitors', 'status']].head())
PY
첫 마스크는 휴관 시간을 표시합니다. 두 번째는 실제 0을 포함해 방문객 수가 기록된 개관 시간을 관측된 것으로 표시합니다. &는 두 조건을 원소별로 결합하고 notna()는 값이 있는지 확인합니다. valid_hour는 관측 조건을 1 또는 0으로 바꿔 합계로 사용 가능한 관측 시간 수를 셀 수 있게 합니다. 누락된 방문객 수를 0으로 채우지 않습니다.
python3 observe.py data results
head -n 10 results/hours.csv
head -n 10은 헤더와 첫 데이터 아홉 행을 표시합니다. 저장된 CSV에는 date,slot,open,visitors,status,valid_hour가 있으며 복합 키순으로 정렬됩니다. Explorer에서도 확인하세요. quiet의 기록된 0은 observed와 valid_hour=1, unlogged의 빈 방문객 수는 missing과 valid_hour=0, closed의 빈 방문객 수는 closed입니다. 빈칸은 상태에 따라 알 수 없음 또는 해당 없음이며, 추측한 0이 아닙니다.
자가 확인: 계획의 각 행이 hours.csv에 정확히 한 번 나타나고, 각 방문객 수가 자신의 날짜와 시간대에 연결됩니다. ValueError가 나오면 어떤 관계가 잘못됐는지 읽으세요. 이 코드와 변경하지 않은 입력 파일을 기준으로 스크립트를 점검하고 수정한 뒤 다시 실행합니다. 터미널을 다시 열 필요는 없습니다.
시간당 방문객 수와 기록률 계산하기
이 단계에서는 알 수 없는 방문객 수의 의미를 바꾸지 않고 시간대별로 요약합니다. 같은 WebIDE 터미널과 작업 폴더에서 계속하세요.
evening에는 개관 예정 네 시간 중 세 시간에 각각 12명의 기록이 있습니다. 합계는 36명, 관측 시간 수는 3, 시간당 방문객 수는 36 / 3 = 12, 기록률은 3 / 4 = 0.75입니다. 즉, 개관 예정 시간의 75%가 기록되었습니다. 누락된 시간은 시간당 수치의 분모에는 들어가지 않지만 기록률은 낮춥니다. morning은 관측 네 시간에 40명이므로 합계는 더 크지만 시간당 수치는 10으로 더 낮습니다.
quiet는 기록된 0 네 개로 합계 0, 시간당 0, 기록률 100%입니다. unlogged는 관측 시간이 없습니다. 기록률은 0이지만 합계와 시간당 수치는 빈칸입니다. closed는 개관 예정 시간이 0이므로 기록률과 시간당 수치 모두 정의되지 않아 빈칸입니다. 나중에 정의되지 않은 값을 높이 0인 막대로 만들면 안 됩니다.
다음 블록을 observe.py에 한 번 추가합니다. >>는 덮어쓰기 대신 이어 쓰기입니다. 위에서 만든 hours와 output을 사용합니다. sum(min_count=1)은 모두 결측이면 합계도 결측으로 반환하지만 기본 합계는 유효한 관측이 없어도 0을 반환합니다. replace(0, float('nan'))은 나누기 전에 0인 분모를 명시적인 결측값으로 바꿉니다. 이때 실제로 관측된 분자의 0은 유지됩니다.
cat >> observe.py <<'PY'
grouped = hours.groupby('slot')
summary = grouped.agg(planned_hours=('open', 'sum'), observed_hours=('valid_hour', 'sum'))
summary['visitors'] = grouped['visitors'].sum(min_count=1)
summary['coverage'] = summary['observed_hours'] / summary['planned_hours'].replace(0, float('nan'))
summary['rate'] = summary['visitors'] / summary['observed_hours'].replace(0, float('nan'))
summary.to_csv(output / 'summary.csv')
print(summary)
PY
python3 observe.py data results
cat results/summary.csv
slot,planned_hours,observed_hours,visitors,coverage,rate
afternoon,3,3,36.0,1.0,12.0
closed,0,0,,,
evening,4,3,36.0,0.75,12.0
morning,4,4,40.0,1.0,10.0
quiet,4,4,0.0,1.0,0.0
short,4,1,20.0,0.25,20.0
sparse,5,3,27.0,0.6,9.0
unlogged,4,0,,0.0,
CSV 숫자는 36 또는 36.0처럼 달리 표시되어도 결과는 같습니다. 필수 열과 의미는 유지됩니다. short의 관측된 시간당 수치가 가장 높지만 관측은 한 시간뿐입니다. sparse는 관측이 세 개라도 계획 시간의 60%만 기록되어 있습니다. 따라서 제안하기 전에 근거의 충분함을 살펴야 합니다. 이 실습에서는 아직 둘 중 어느 것도 우승자로 정하지 않습니다.
Explorer에서 results/summary.csv를 여세요. data/plan.csv, data/counts.csv, data/equal.csv와 스크립트를 보존합니다. 재사용 가능한 호출 규약은 python3 observe.py INPUT_DIRECTORY OUTPUT_DIRECTORY입니다. 입력에서 두 CSV를 읽고 출력에 hours.csv와 summary.csv를 씁니다. 모든 키와 수치 관계를 보존하면 동등한 계산과 다른 출력 행 순서도 허용됩니다. 추가하려다 스크립트를 덮어썼다면 2단계 스크립트를 다시 만든 뒤 이 블록을 한 번 추가하고 실행하세요.
자가 확인: 관측하지 않은 시간대와 실제 관측 결과가 0인 시간대는 합계, 분모, 시간당 수치가 다릅니다. 다음 실습에서는 이 구분을 사용해 비교할 관측 근거가 충분한지 판단합니다.
요약
관측 기회가 같은 비교 그래프를 만들고, 완전한 복합 키로 방문객 수를 연결하고, 시간별·시간대별 CSV 파일을 만들었습니다. 문을 닫은 시간, 누락된 기록, 실제 0은 여전히 구분됩니다. 시간당 방문객 수에는 기록된 개관 시간을 사용하고, 기록률은 계획된 관측 기회 중 실제 기록된 비율을 보여 줍니다.
재사용 가능한 분석과 원본 데이터를 보관하세요. 과거 관측을 요약한 이 시간당 수치는 개관 시간을 늘린 뒤 무슨 일이 생길지 입증하지 않습니다. 다음 독립 환경에서는 제안하기 전에 명시적인 관측 근거 규칙을 적용합니다.



