final_1

cat("\014")  # 콘솔 클리어(폼피드)
rm(list = ls())
getwd()
[1] "/Users/gohanseo/Desktop/R"
setwd("~/Desktop/R")
# 경로 설정
options(repos = c(CRAN = "https://cloud.r-project.org"))
install.packages("showtext", dependencies = TRUE)

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("tidyverse")

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("psych")

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("zoo")

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("patchwork")

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("lubridate")

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("ggh4x")     # facet_wrap2: 각 패널에 축 표시

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("ggrepel")   # 라벨 겹침 최소화

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.1     ✔ stringr   1.5.2
✔ ggplot2   4.0.0     ✔ tibble    3.3.0
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.1.0     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(showtext)
Loading required package: sysfonts
Loading required package: showtextdb
library(psych)

Attaching package: 'psych'

The following objects are masked from 'package:ggplot2':

    %+%, alpha
library(dplyr)
library(zoo)

Attaching package: 'zoo'

The following objects are masked from 'package:base':

    as.Date, as.Date.numeric
library(ggplot2)
font_add(family = "AppleGothic", regular = "/System/Library/Fonts/AppleGothic.ttf")
showtext_auto()
theme_set(theme_minimal(base_family = "AppleGothic"))
data <- read_csv("final.csv")
Rows: 32473 Columns: 21
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr   (2): city, type
dbl  (18): time, hum, rain, tem, kr, snack, cafe, jp, fish, chicken, pizza, ...
date  (1): date

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
head(data, 10)    # 앞부분 10행 보기
# A tibble: 10 × 21
   date        time   hum  rain   tem    kr snack  cafe    jp  fish chicken
   <date>     <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>   <dbl>
 1 2019-07-12    18    91     0  22.6     0     0     1     0     0       0
 2 2019-07-19    21    63     0  28.6     0     0     0     0     0       0
 3 2019-07-22    12    92     0  25.9     0     0     0     0     0       0
 4 2019-07-22    15    96     0  24.4     0     0     0     0     0       0
 5 2019-07-22    18    94     0  24.7     0     0     1     0     0       0
 6 2019-07-28    14    92     0  26.7     0     0     1     0     0       0
 7 2019-08-03    11    99     0  27.5     0     0     0     0     0       1
 8 2019-08-04    15    98     0  26.9     0     0     1     0     0       0
 9 2019-08-05    10    77     0  31.7     0     0     1     0     0       0
10 2019-08-05    11    77     0  31      19     8     1     7     0       0
# ℹ 10 more variables: pizza <dbl>, asian_western <dbl>, ch <dbl>, pig <dbl>,
#   night <dbl>, steam <dbl>, boxed <dbl>, fast <dbl>, city <chr>, type <chr>
str(data)
spc_tbl_ [32,473 × 21] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ date         : Date[1:32473], format: "2019-07-12" "2019-07-19" ...
 $ time         : num [1:32473] 18 21 12 15 18 14 11 15 10 11 ...
 $ hum          : num [1:32473] 91 63 92 96 94 92 99 98 77 77 ...
 $ rain         : num [1:32473] 0 0 0 0 0 0 0 0 0 0 ...
 $ tem          : num [1:32473] 22.6 28.6 25.9 24.4 24.7 26.7 27.5 26.9 31.7 31 ...
 $ kr           : num [1:32473] 0 0 0 0 0 0 0 0 0 19 ...
 $ snack        : num [1:32473] 0 0 0 0 0 0 0 0 0 8 ...
 $ cafe         : num [1:32473] 1 0 0 0 1 1 0 1 1 1 ...
 $ jp           : num [1:32473] 0 0 0 0 0 0 0 0 0 7 ...
 $ fish         : num [1:32473] 0 0 0 0 0 0 0 0 0 0 ...
 $ chicken      : num [1:32473] 0 0 0 0 0 0 1 0 0 0 ...
 $ pizza        : num [1:32473] 0 0 0 0 0 0 0 0 0 4 ...
 $ asian_western: num [1:32473] 0 0 0 0 0 0 0 0 0 3 ...
 $ ch           : num [1:32473] 0 0 0 0 0 0 0 0 0 5 ...
 $ pig          : num [1:32473] 0 0 1 0 0 0 0 0 0 0 ...
 $ night        : num [1:32473] 0 2 0 0 0 0 0 0 0 0 ...
 $ steam        : num [1:32473] 0 0 0 0 0 0 0 0 0 1 ...
 $ boxed        : num [1:32473] 0 0 0 0 0 0 0 0 0 0 ...
 $ fast         : num [1:32473] 0 0 0 1 0 0 0 0 0 15 ...
 $ city         : chr [1:32473] "경기도고양시덕양구" "경기도고양시덕양구" "경기도고양시덕양구" "경기도고양시덕양구" ...
 $ type         : chr [1:32473] "흐림" "보통" "흐림" "흐림" ...
 - attr(*, "spec")=
  .. cols(
  ..   date = col_date(format = ""),
  ..   time = col_double(),
  ..   hum = col_double(),
  ..   rain = col_double(),
  ..   tem = col_double(),
  ..   kr = col_double(),
  ..   snack = col_double(),
  ..   cafe = col_double(),
  ..   jp = col_double(),
  ..   fish = col_double(),
  ..   chicken = col_double(),
  ..   pizza = col_double(),
  ..   asian_western = col_double(),
  ..   ch = col_double(),
  ..   pig = col_double(),
  ..   night = col_double(),
  ..   steam = col_double(),
  ..   boxed = col_double(),
  ..   fast = col_double(),
  ..   city = col_character(),
  ..   type = col_character()
  .. )
 - attr(*, "problems")=<externalptr> 

형 변환

# ----------------------------
# 💡 초간단 형변환 코드 (R 기본버전)
# ----------------------------

# 1️⃣ 데이터 불러오기
df <- read.csv("final.csv", stringsAsFactors = FALSE)

# 2️⃣ 컬럼 이름 소문자로
names(df) <- tolower(names(df))

# 3️⃣ 열별 형변환
df$date <- as.Date(df$date)           # 날짜
df$time <- as.numeric(df$time)        # 숫자 (시간)

# 숫자형 데이터들
num_cols <- c("hum","rain","tem","kr","snack","cafe","jp","fish",
              "chicken","pizza","asian_western","ch","pig",
              "night","steam","boxed","fast")
df[num_cols] <- lapply(df[num_cols], as.numeric)

# 범주형 데이터들
df$city <- as.factor(df$city)
df$type <- as.factor(df$type)

# 4️⃣ 결과 확인
str(df)
'data.frame':   32473 obs. of  21 variables:
 $ date         : Date, format: "2019-07-12" "2019-07-19" ...
 $ time         : num  18 21 12 15 18 14 11 15 10 11 ...
 $ hum          : num  91 63 92 96 94 92 99 98 77 77 ...
 $ rain         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ tem          : num  22.6 28.6 25.9 24.4 24.7 26.7 27.5 26.9 31.7 31 ...
 $ kr           : num  0 0 0 0 0 0 0 0 0 19 ...
 $ snack        : num  0 0 0 0 0 0 0 0 0 8 ...
 $ cafe         : num  1 0 0 0 1 1 0 1 1 1 ...
 $ jp           : num  0 0 0 0 0 0 0 0 0 7 ...
 $ fish         : num  0 0 0 0 0 0 0 0 0 0 ...
 $ chicken      : num  0 0 0 0 0 0 1 0 0 0 ...
 $ pizza        : num  0 0 0 0 0 0 0 0 0 4 ...
 $ asian_western: num  0 0 0 0 0 0 0 0 0 3 ...
 $ ch           : num  0 0 0 0 0 0 0 0 0 5 ...
 $ pig          : num  0 0 1 0 0 0 0 0 0 0 ...
 $ night        : num  0 2 0 0 0 0 0 0 0 0 ...
 $ steam        : num  0 0 0 0 0 0 0 0 0 1 ...
 $ boxed        : num  0 0 0 0 0 0 0 0 0 0 ...
 $ fast         : num  0 0 0 1 0 0 0 0 0 15 ...
 $ city         : Factor w/ 5 levels "경기도고양시덕양구",..: 1 1 1 1 1 1 1 1 1 1 ...
 $ type         : Factor w/ 6 levels "눈","맑음","보통",..: 6 3 6 6 6 6 6 6 3 3 ...
# (선택) 저장
# write.csv(df, "final_typed.csv", row.names = FALSE)

SUMMARY

summary(df)
      date                 time            hum              rain        
 Min.   :2019-07-01   Min.   : 0.00   Min.   :  0.00   Min.   : 0.0000  
 1st Qu.:2019-10-14   1st Qu.: 9.00   1st Qu.: 59.00   1st Qu.: 0.0000  
 Median :2020-01-09   Median :14.00   Median : 76.00   Median : 0.0000  
 Mean   :2020-01-08   Mean   :13.22   Mean   : 73.42   Mean   : 0.1225  
 3rd Qu.:2020-04-06   3rd Qu.:19.00   3rd Qu.: 92.00   3rd Qu.: 0.0000  
 Max.   :2020-06-30   Max.   :23.00   Max.   :100.00   Max.   :31.5000  
      tem               kr             snack             cafe        
 Min.   :-13.30   Min.   :  0.00   Min.   :  0.00   Min.   :  0.000  
 1st Qu.:  4.20   1st Qu.:  3.00   1st Qu.:  2.00   1st Qu.:  1.000  
 Median : 12.10   Median : 10.00   Median : 11.00   Median :  4.000  
 Mean   : 12.38   Mean   : 18.63   Mean   : 14.53   Mean   :  8.332  
 3rd Qu.: 21.00   3rd Qu.: 25.00   3rd Qu.: 22.00   3rd Qu.: 12.000  
 Max.   : 36.20   Max.   :258.00   Max.   :128.00   Max.   :122.000  
       jp              fish           chicken           pizza       
 Min.   : 0.000   Min.   : 0.000   Min.   :  0.00   Min.   : 0.000  
 1st Qu.: 1.000   1st Qu.: 0.000   1st Qu.:  4.00   1st Qu.: 1.000  
 Median : 5.000   Median : 1.000   Median : 15.00   Median : 4.000  
 Mean   : 7.983   Mean   : 2.625   Mean   : 29.77   Mean   : 5.785  
 3rd Qu.:12.000   3rd Qu.: 3.000   3rd Qu.: 41.00   3rd Qu.: 8.000  
 Max.   :70.000   Max.   :52.000   Max.   :307.00   Max.   :58.000  
 asian_western         ch              pig             night        
 Min.   : 0.00   Min.   : 0.000   Min.   :  0.00   Min.   :  0.000  
 1st Qu.: 0.00   1st Qu.: 0.000   1st Qu.:  2.00   1st Qu.:  0.000  
 Median : 2.00   Median : 2.000   Median :  7.00   Median :  3.000  
 Mean   : 3.25   Mean   : 4.337   Mean   : 16.89   Mean   :  6.261  
 3rd Qu.: 5.00   3rd Qu.: 6.000   3rd Qu.: 19.00   3rd Qu.:  7.000  
 Max.   :39.00   Max.   :54.000   Max.   :301.00   Max.   :107.000  
     steam           boxed             fast                       city     
 Min.   : 0.00   Min.   : 0.000   Min.   : 0.00   경기도고양시덕양구:5237  
 1st Qu.: 0.00   1st Qu.: 0.000   1st Qu.: 1.00   경기도광명시      :6142  
 Median : 2.00   Median : 0.000   Median : 8.00   경기도부천시      :5952  
 Mean   : 5.41   Mean   : 1.599   Mean   :10.17   경기도의정부시    :8654  
 3rd Qu.: 7.00   3rd Qu.: 2.000   3rd Qu.:16.00   서울특별시구로구  :6488  
 Max.   :68.00   Max.   :60.000   Max.   :68.00                            
       type      
 눈      :   12  
 맑음    : 4383  
 보통    :13413  
 비      : 1643  
 진눈개비:   43  
 흐림    :12979  

요약

이 데이터는 2019년 7월부터 2020년 6월까지 1년간의 배달 주문 정보를 담고 있다.
총 32,473개의 데이터로 구성되어 있으며, 날짜·시간·날씨·메뉴별 주문량을 포함한다.

주문 시간은 0시부터 23시까지 분포하며, 평균은 오후 1시경으로 점심과 저녁 시간대에 주문이 집중되어 있다.

기온은 평균 12.4도로 사계절이 모두 포함되어 있으며, 습도는 평균 73%로 전반적으로 습한 날이 많았다. 강수량은 대부분 0mm로 비가 오지 않는 날이 많았으나, 일부 최대 30mm 이상의 강우도 관측되었다.

메뉴별 주문량을 보면 치킨이 평균 약 30건으로 가장 많았고, 그 뒤로 한식(18.6건), 고기류(16.9건), 분식(14.5건), 패스트푸드(10.2건) 순으로 주문이 많았다. 카페나 일식은 중간 수준의 수요를 보였고, 도시락이나 찜, 생선류는 비교적 낮은 주문량을 보였다.

도시 변수는 데이터 누락이 없는 지역만을 선별하여 구성하였기 때문에, 본 분석에서는 도시 간 비교보다는 날씨·시간·메뉴와 같은 요인에 초점을 맞추었다.

전체적으로 이 데이터는 점심과 저녁 시간대 중심의 주문 패턴을 보여주며,
기온·습도·강수 여부에 따라 메뉴별 주문 경향이 달라질 가능성이 높다.
특히 치킨, 한식, 고기류는 전반적으로 꾸준히 인기 있는 메뉴로 나타났다.

DESCRIBE

describe(df)
Warning in FUN(newX[, i], ...): no non-missing arguments to min; returning Inf
Warning in FUN(newX[, i], ...): no non-missing arguments to max; returning -Inf
              vars     n  mean    sd median trimmed   mad   min   max range
date             1 32473   NaN    NA     NA     NaN    NA   Inf  -Inf  -Inf
time             2 32473 13.22  6.86   14.0   13.65  7.41   0.0  23.0  23.0
hum              3 32473 73.42 20.34   76.0   75.33 25.20   0.0 100.0 100.0
rain             4 32473  0.12  0.95    0.0    0.00  0.00   0.0  31.5  31.5
tem              5 32473 12.38  9.96   12.1   12.41 12.45 -13.3  36.2  49.5
kr               6 32473 18.63 23.99   10.0   13.62 13.34   0.0 258.0 258.0
snack            7 32473 14.53 14.48   11.0   12.42 14.83   0.0 128.0 128.0
cafe             8 32473  8.33 10.24    4.0    6.39  5.93   0.0 122.0 122.0
jp               9 32473  7.98  9.06    5.0    6.38  7.41   0.0  70.0  70.0
fish            10 32473  2.63  4.34    1.0    1.63  1.48   0.0  52.0  52.0
chicken         11 32473 29.77 38.83   15.0   21.89 20.76   0.0 307.0 307.0
pizza           12 32473  5.78  6.26    4.0    4.73  4.45   0.0  58.0  58.0
asian_western   13 32473  3.25  4.11    2.0    2.50  2.97   0.0  39.0  39.0
ch              14 32473  4.34  6.01    2.0    3.08  2.97   0.0  54.0  54.0
pig             15 32473 16.89 27.53    7.0   10.78  8.90   0.0 301.0 301.0
night           16 32473  6.26 10.45    3.0    3.80  4.45   0.0 107.0 107.0
steam           17 32473  5.41  8.07    2.0    3.58  2.97   0.0  68.0  68.0
boxed           18 32473  1.60  3.34    0.0    0.81  0.00   0.0  60.0  60.0
fast            19 32473 10.17  9.79    8.0    8.92 11.86   0.0  68.0  68.0
city*           20 32473  3.15  1.37    3.0    3.19  1.48   1.0   5.0   4.0
type*           21 32473  4.12  1.59    3.0    4.15  1.48   1.0   6.0   5.0
               skew kurtosis   se
date             NA       NA   NA
time          -0.50    -0.78 0.04
hum           -0.73     0.03 0.11
rain          15.14   300.79 0.01
tem           -0.01    -1.06 0.06
kr             2.25     6.22 0.13
snack          1.32     2.18 0.08
cafe           1.84     4.43 0.06
jp             1.65     3.21 0.05
fish           2.88    11.56 0.02
chicken        2.34     6.97 0.22
pizza          1.80     4.50 0.03
asian_western  1.68     3.64 0.02
ch             2.13     5.50 0.03
pig            3.69    17.78 0.15
night          3.19    12.53 0.06
steam          2.52     7.48 0.04
boxed          4.16    26.12 0.02
fast           0.96     0.60 0.05
city*         -0.19    -1.22 0.01
type*          0.20    -1.70 0.01

요약

이 데이터는 총 32,473개의 관측값과 21개의 변수로 구성된 시계열 데이터로, 2019년 7월부터 2020년 6월까지의 배달 주문을 날짜·시간·날씨·메뉴별로 기록하고 있다.

시간(time)은 0시부터 23시까지 분포하며 평균은 오후 1시로, 점심과 저녁 시간대에 주문이 집중되어 있다.
날씨 변수 중 습도(hum)는 평균 73%로 습한 날이 많았고, 강수량(rain)은 대부분 0mm로 비가 오지 않았으나 간헐적으로 최대 31.5mm의 강우가 있었다. 기온(tem)은 평균 12.4도로 사계절이 모두 포함된 고른 분포를 보인다.

메뉴별 주문량은 전반적으로 양의 왜도(skew > 0)를 가지며, 대부분의 날에는 주문이 적지만 특정 시간이나 날씨 조건에서 주문이 급격히 증가하는 비대칭 분포를 보인다.
치킨(chicken)은 평균 29.8건으로 가장 높은 주문량을 기록했으며, 그 뒤로 한식(kr, 평균 18.6건), 고기류(pig, 평균 16.9건), 분식(snack, 평균 14.5건), 패스트푸드(fast, 평균 10.2건) 순으로 수요가 많았다. 카페(cafe), 일식(jp), 중식(ch) 등은 중간 수준, 도시락(boxed)이나 생선(fish)류는 상대적으로 적은 편이다.

도시(city)는 결측이 없는 지역만 선별되어 있으며, 분석의 초점은 도시 비교가 아닌 날씨·시간·메뉴 간 관계에 맞춰져 있다.
날씨 유형(type)은 ’보통’과 ’흐림’이 가장 높은 빈도를 보여 대체로 일상적인 날씨 속에서의 주문 패턴이 중심을 이루고 있다.

추후 분석 계획

향후 분석에서는 도시(city) 변수를 제외하고, 날짜·시간·날씨 요인에 따른 주문량 변화를 중심으로 탐색을 진행할 예정이다. 특히 날씨 요인이 메뉴 선택이나 주문 시간대에 어떤 영향을 주는지를 중점적으로 살펴보며, 각 변수 간의 관계를 시각화와 통계 요약을 통해 비교 분석할 계획이다.


  • 월별 주문량 변화 (date)
    • 날짜를 월 단위로 변환하여 1년간의 주문 추이를 분석한다.
    • 여름(7~8월), 겨울(12~2월) 등 계절 간 주문량 차이를 비교하고, 특정 시기(휴가철, 명절 등)에 주문이 증가하는 메뉴군을 확인한다.
  • 시간별 주문량 변화 (time)
    • 시간 변수를 기준으로 하루 내 주문 패턴을 시각화한다.
    • 점심(11~14시), 저녁(17~20시), 야식(22~01시) 등 주요 시간대별 인기 메뉴를 비교한다.
  • 습도별 주문량 변화 (hum)
    • 습도를 3단계(건조/보통/습함)로 구간화하여 습도 수준에 따른 주문량 변화 및 메뉴 선호도 차이를 분석한다.
  • 강수량별 주문량 변화 (rain)
    • 비가 오는 날(rain > 0)과 오지 않는 날(rain = 0)을 구분해 강수 여부가 주문 패턴에 미치는 영향을 살펴본다.
    • 예를 들어, 비 오는 날 치킨이나 찜류 주문이 늘어나는지를 확인한다.
  • 온도별 주문량 변화 (tem)
    • 온도를 저온(5°C 이하)·보통(5~20°C)·고온(20°C 이상)으로 나누어 기온별 메뉴 주문 경향을 비교한다.
    • 더운 날에는 냉음식·카페류, 추운 날에는 찜·한식류 주문이 증가하는지를 분석한다.
  • 날씨별 주문량 변화 (type)
    • 날씨 유형(맑음, 흐림, 비, 눈 등)에 따른 전체 주문량 및 메뉴별 차이를 분석한다.
    • 특정 날씨 조건에서 주문이 증가하는 메뉴군(예: 치킨, 한식, 분식 등)을 중심으로 날씨 기반 추천 UX로 확장할 수 있다.

분석 과정에서는 각 구간별(예: 월별, 시간대별, 날씨별) 데이터의 개수가 다르기 때문에, 표본 수 차이로 인한 비교 왜곡을 방지하기 위해 보정(normalization) 과정을 병행할 예정이다. 예를 들어, scale(), prop.table(), mutate(비율 = 값 / 합계) 등의 방식을 활용하여 데이터 개수 차이를 보정한 후 각 그룹 간 비교를 수행할 계획이다.

DATE (1)

# ── 패키지
suppressPackageStartupMessages({
  library(dplyr); library(lubridate); library(ggplot2)
  library(ggh4x); library(ggrepel); library(showtext); library(tibble)
})

## 1) 폰트: Google Fonts에서 Noto Sans KR 등록 + 활성화
font_add_google("Noto Sans KR", family = "noto")
showtext_auto(TRUE)   # on-screen & ggsave에 적용

## 2) 총주문량 만들기
menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")
df$total_orders <- rowSums(df[, intersect(menu_cols, names(df)), drop = FALSE], na.rm = TRUE)

## 3) 날짜별 합계 + 보조 컬럼
daily <- df %>%
  group_by(date) %>%
  summarise(total_orders = sum(total_orders, na.rm = TRUE), .groups = "drop") %>%
  mutate(
    ym   = format(date, "%Y-%m"),
    dom  = day(date),
    wday = wday(date, week_start = 1)     # 1=월 … 7=일
  )

## 4) 공휴일(2019-07 ~ 2020-06) + 라벨
holidays_df <- tibble(
  date = as.Date(c(
    "2019-08-15","2019-09-12","2019-09-13","2019-09-14",
    "2019-10-03","2019-10-09","2019-12-25",
    "2020-01-01","2020-01-24","2020-01-25","2020-01-26","2020-01-27",
    "2020-03-01","2020-04-15","2020-04-30","2020-05-05","2020-06-06"
  )),
  hol_name = c(
    "광복절","추석연휴","추석","추석연휴",
    "개천절","한글날","성탄절",
    "신정","설연휴","설날","설연휴","대체공휴일(설)",
    "삼일절","국회의원선거","부처님오신날","어린이날","현충일"
  )
)

daily <- daily %>%
  left_join(holidays_df, by = "date") %>%
  mutate(
    is_weekend = wday %in% c(6,7),
    day_type = case_when(
      !is.na(hol_name) ~ "공휴일",
      is_weekend       ~ "주말",
      TRUE             ~ "평일"
    ),
    day_type = factor(day_type, levels = c("평일","주말","공휴일"))
  )

## 5) 그래프
p <- ggplot(daily, aes(x = dom, y = total_orders)) +
  geom_line(linewidth = 0.4, aes(group = ym), alpha = 0.9) +
  geom_point(aes(color = day_type), size = 1.5, alpha = 0.95) +
  ggrepel::geom_text_repel(
    data = subset(daily, !is.na(hol_name)),
    aes(label = hol_name),
    family = "noto", size = 3, seed = 42,
    min.segment.length = 0, box.padding = 0.25, point.padding = 0.15,
    max.overlaps = 200
  ) +
  ggh4x::facet_wrap2(vars(ym), ncol = 4, scales = "free_y", axes = "x") +
  scale_x_continuous(breaks = 1:31, minor_breaks = NULL, expand = c(0.01, 0.01)) +
  scale_color_manual(values = c("평일"="#1f77b4","주말"="#ff7f0e","공휴일"="#d62728")) +
  labs(
    title = "월별 주문량 변화 (일 단위, 평일·주말·공휴일 구분)",
    subtitle = "각 패널은 한 달. 점 색으로 평일/주말/공휴일 구분, 공휴일 이름 라벨 표시",
    x = "일(day of month)", y = "일별 주문량 합계", color = "구분"
  ) +
  theme_minimal(base_size = 11) +
  theme(
    text = element_text(family = "noto"),
    strip.text = element_text(face = "bold"),
    panel.grid.minor = element_blank(),
    axis.text.x = element_text(size = 7),
    panel.spacing.x = grid::unit(0.08, "lines"),
    panel.spacing.y = grid::unit(0.5, "lines")
  )

print(p)

# (선택) 저장 시에도 한글 유지
# ggsave("월별주문량.png", p, width = 12, height = 8, dpi = 150)

요약

  • 전반적으로 주말·공휴일에 주문량이 상승한다. 점(주말/공휴일)이 평일보다 위쪽에 찍히는 패턴이 반복된다.

  • 다만 가족 중심의 명절형 공휴일(설, 추석, 신정)은 오히려 하향세가 뚜렷하다. 가족 모임·이동과 일부 매장 휴무로 수요·공급이 동시에 줄어드는 유형으로 보인다.

  • 반대로 어린이날·선거일처럼 ‘외출/여가형’ 성격이 강한 공휴일은 크게 떨어지지 않거나 유지/상승하는 장면이 보인다.

  • (주의) 현재 facet이 달마다 y축이 다르다(free_y). 달 간 절대 높낮이 비교는 조심하고, 월 내부의 추세 해석에 초점을 두자.

추후 분석

  • 공휴일 타입 분류 비교 : 명절형(설/추석/신정) vs 여가형(어린이날, 선거, 부처님오신날 등) 으로 나눠 주문량·메뉴 비중 변화를 비교. 명절 전·당일·다음날(±1일) 치환/보상 효과도 함께 확인.

  • 평일 급상승일(스파이크) 원인 파악 : 평일인데 튀는 날 리스트업 후 날씨(폭우·폭염·한파), 월급일(20~25일), 프로모션/이벤트, 사회적 이슈 등 후보 요인을 대조. 스파이크 전후 하루(±1)까지 함께 살펴 지속/반동 여부 확인.

  • 시간대 × 요일/공휴일 교차 패턴 : 주말/평일/공휴일 각각에서 점심·저녁·야식 피크 시간대가 어떻게 이동하는지, 피크 폭이 얼마나 다른지 비교.

  • 메뉴 믹스 변화(비율 기준) : 전체량뿐 아니라 치킨·한식·분식 등 상위 메뉴군의 점유율 변화를 주말/공휴일/명절별로 비교. 명절형 공휴일에서 어떤 메뉴가 특히 줄거나 늘었는지 식별.

  • 계절성과 날씨 임계치 효과 : 계절(여름/겨울)별로 같은 날씨 조건에서의 반응 차이 비교(예: 여름 폭우 vs 겨울 강설). 임계치(비>0, 기온≥25℃/≤0℃ 등)**를 기준으로 주문 변화의 방향성 점검.

DATE (2)

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales); library(forcats); library(ggrepel)
})

# 0) 메뉴 열 정의
menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")

# 1) 날짜별 합계(일 단위)
daily <- df %>%
  mutate(date = as.Date(date)) %>%
  group_by(date) %>%
  summarise(across(all_of(menu_cols), ~sum(.x, na.rm = TRUE)), .groups = "drop") %>%
  mutate(day_total = rowSums(across(all_of(menu_cols))),
         month = floor_date(date, "month"))

# ────────────────────────────────────────────────────────────
# A) 월 × 메뉴 **점유율(%) 히트맵**
# ────────────────────────────────────────────────────────────
monthly_share <- daily %>%
  group_by(month) %>%
  summarise(across(all_of(menu_cols), ~sum(.x, na.rm = TRUE)), .groups = "drop") %>%
  mutate(month_total = rowSums(across(all_of(menu_cols)))) %>%
  mutate(across(all_of(menu_cols), ~ .x / ifelse(month_total == 0, NA_real_, month_total))) %>%
  select(-month_total) %>%
  pivot_longer(all_of(menu_cols), names_to = "menu", values_to = "share")

# 메뉴 표시 순서(평균 점유율 높은 메뉴가 위로)
menu_order <- monthly_share %>%
  group_by(menu) %>% summarise(avg = mean(share, na.rm = TRUE), .groups="drop") %>%
  arrange(desc(avg)) %>% pull(menu)
monthly_share$menu <- factor(monthly_share$menu, levels = rev(menu_order))

p_heat_month <- ggplot(monthly_share, aes(x = month, y = menu, fill = share)) +
  geom_tile() +
  scale_fill_viridis_c(labels = percent_format(accuracy = 1)) +
  scale_x_date(date_breaks = "1 month", date_labels = "%Y-%m", expand = c(0.01, 0.01)) +
  labs(title = "월×메뉴 점유율(%) 히트맵", x = NULL, y = NULL, fill = "점유율") +
  theme_minimal(base_size = 11) +
  theme(panel.grid = element_blank(),
        axis.text.x = element_text(angle = 90, vjust = 0.5))
print(p_heat_month)

요약

  • 전체 구조

    • 상위권 고정: chickenkrpigsnackfast 순서가 대부분의 달에서 유지.

    • 변동 폭은 크지 않지만 계절·시기별로 소폭의 점유율 이동이 보임.

  • 메뉴별/계절 패턴

    • chicken: 연중 최고 비중(≈20%대) 을 안정적으로 유지. 카테고리의 앵커 역할.

    • kr(한식): 중상위권에서 큰 흔들림 없이 견조한 비중.

    • pig(고기류): 완만한 상승 경향(연말~초봄 구간에서 특히 강세).

    • snack(분식): 가을(9–10월) 무렵 상대적 비중↑, 이후 완만히 보합/소폭 하락.

    • fast(패스트푸드): 점진적 하락 기조(2020년 들어 색이 옅어짐).

    • cafe: 하반기 초반(2019-07~09) 대비 겨울에 비중↓—차가운 디저트/음료 수요 약화로 해석 가능.

    • jp(일식): 중하위권에서 완만한 하락/보합.

    • night(야식): 전반적으로 보합, 2020년 봄에 소폭 약화.

    • steam(찜류): 겨울철(12–2월) 비중↑ 후 봄에 다시 ↓ — 뚜렷한 계절성.

    • pizza / ch / asian_western / fish / boxed: 낮은 비중에서 대체로 보합~미세 하락.

추후 분석

  • 시간(0–23시)×메뉴 히트맵으로 평일/주말의 피크 시간대 차이 확인.

  • 명절 포함 달(1·9월) 라벨링해서 점유율 변화가 실제로 컸는지 재확인.

  • 상위 K개 메뉴 지수화(=100) 라인으로 변곡점(스파이크/디프) 체크.

TIME (1)

suppressPackageStartupMessages({
  library(dplyr); library(lubridate); library(ggplot2); library(scales)
})

# 0) 메뉴 열
menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")

# 1) 시간(정시) · 날짜별 총주문 만들기
df_hour <- df %>%
  mutate(
    date = as.Date(date),
    hour = pmax(0, pmin(23, as.integer(time))),                # 0~23시
    total_orders = rowSums(across(all_of(menu_cols)), na.rm = TRUE),
    day_type = if_else(wday(date, week_start = 1) %in% c(6,7), "주말", "평일")
  ) %>%
  group_by(date, hour, day_type) %>%
  summarise(total = sum(total_orders, na.rm = TRUE), .groups = "drop")

## B) 평일 vs 주말 비교(겹쳐보기)
ww_hour <- df_hour %>%
  group_by(day_type, hour) %>%
  summarise(mean = mean(total, na.rm = TRUE), .groups = "drop")

ggplot(ww_hour, aes(hour, mean, color = day_type)) +
  geom_line(linewidth = 0.8) +
  geom_point(size = 1.6) +
  scale_x_continuous(breaks = 0:23) +
  labs(title = "시간대별 평균 주문량 — 평일 vs 주말",
       x = "시간", y = "평균 주문량", color = "구분") +
  theme_minimal()

요약

  • 레벨 차이: 거의 전 시간대에서 주말 > 평일. 특히 점심·저녁 시간대 격차가 큼.

  • 타이밍 차이: 주말은 저녁 피크가 1시간 가량 늦게 나타나고, 야간 꼬리도 길다.

  • 패턴 안정성: 하루의 전형적인 곡선(점심 피크 → 오후 슬럼프 → 저녁 초대형 피크)이 양쪽 모두 일관.

추후 분석

  • 시간×메뉴 교차 히트맵(평일/주말 패싯) : 어떤 메뉴가 점심·저녁·야식에서 강한지 한눈에 파악(예: 치킨=저녁, 카페=오전/오후).

  • 계절/날씨와의 상호작용 : 여름·겨울 또는 비/폭염/한파일에 하루 곡선이 얼마나 이동/증폭되는지 비교.

  • 공휴일/명절의 시간대 패턴 : 일반 공휴일 vs 명절형 공휴일(설·추석) 에서 피크 시점·크기가 어떻게 달라지는지.

TIME (2)

install.packages("tidyr")

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("lubridate")

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("scales")

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
install.packages("tidytext") 

The downloaded binary packages are in
    /var/folders/rn/8twwrhcn2fq04yz856ztjn6r0000gn/T//RtmpJ6akMz/downloaded_packages
# 0) 패키지
suppressPackageStartupMessages({
  library(showtext); library(sysfonts); library(systemfonts); library(ggplot2)
})

# 1) 'noto' 별칭 등록 (온라인 → 오프라인 폴백)
ok <- FALSE
try({
  showtext::font_add_google("Noto Sans KR", family = "noto")
  ok <- TRUE
}, silent = TRUE)

if (!ok) {
  # 시스템에 있는 한글 폰트 중 사용 가능 폰트 탐색 후 'noto' 별칭으로 바인딩
  cand <- c("Noto Sans KR","Apple SD Gothic Neo","Malgun Gothic","NanumGothic")
  fams <- unique(systemfonts::system_fonts()$family)
  pick <- intersect(cand, fams)[1]
  if (!is.na(pick)) {
    sysfonts::font_add(family = "noto", regular = systemfonts::match_font(pick)$path)
    ok <- TRUE
  }
}
Warning: `match_font()` was deprecated in systemfonts 1.1.0.
ℹ Please use `match_fonts()` instead.
showtext::showtext_auto(TRUE)

# 2) 전역 테마에 폰트 적용 (+ 텍스트 지오메트리 기본값도 세팅)
ggplot2::theme_set(theme_minimal(base_size = 11, base_family = "noto"))
ggplot2::update_geom_defaults("text",  list(family = "noto"))
ggplot2::update_geom_defaults("label", list(family = "noto"))

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales); library(tidytext)
})

menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")
TOP_N <- 8

# ── 시간대 라벨: 아침/점심/오후틈/저녁/야식
win_levels <- c("아침(07–10시)", "점심(11–14시)", "오후틈(15–16시)",
                "저녁(17–20시)", "야식(22–01시)")

df_tidy <- df %>%
  mutate(
    date  = as.Date(date),
    hour  = pmax(0, pmin(23, as.integer(time))),
    window = dplyr::case_when(
      hour %in% 7:10            ~ "아침(07–10시)",
      hour %in% 11:14           ~ "점심(11–14시)",
      hour %in% 15:16           ~ "오후틈(15–16시)",   # 점심과 저녁 사이 갭
      hour %in% 17:20           ~ "저녁(17–20시)",
      hour %in% c(22, 23, 0, 1) ~ "야식(22–01시)",
      TRUE ~ NA_character_
    )
  ) %>%
  filter(!is.na(window)) %>%
  mutate(window = factor(window, levels = win_levels)) %>%
  select(date, hour, window, all_of(menu_cols)) %>%
  pivot_longer(all_of(menu_cols), names_to = "menu", values_to = "qty")

# ── 집계: 주문량 & 점유율
win_menu <- df_tidy %>%
  group_by(window, menu) %>%
  summarise(orders = sum(qty, na.rm = TRUE), .groups = "drop") %>%
  group_by(window) %>%
  mutate(share = orders / sum(orders)) %>%
  ungroup()

# ── 각 시간대 TOP-N
top_orders <- win_menu %>% group_by(window) %>% slice_max(orders, n = TOP_N, with_ties = FALSE) %>% ungroup()
top_share  <- win_menu %>% group_by(window) %>% slice_max(share,  n = TOP_N, with_ties = FALSE) %>% ungroup()

# ── [주문량] TOP-N
p_count <- top_orders %>%
  mutate(menu_in = tidytext::reorder_within(menu, orders, window)) %>%
  ggplot(aes(x = orders, y = menu_in, fill = window)) +
  geom_col(width = 0.7, show.legend = FALSE) +
  tidytext::scale_y_reordered() +
  facet_wrap(~ window, ncol = 3, scales = "free_y") +
  scale_x_continuous(labels = label_number(accuracy = 1, scale_cut = cut_short_scale())) +
  labs(title = "주요 시간대별 인기 메뉴 (주문량 TOP-N)",
       subtitle = paste0("TOP-", TOP_N, " 기준 · 시간대: 아침/점심/오후틈/저녁/야식"),
       x = "주문량(합계)", y = NULL) +
  theme_minimal(base_size = 11) +
  theme(panel.grid.minor = element_blank())

print(p_count)

요약

  • 점심(11–14시) : kr(한식)이 1위, 그다음 snack(분식), fast, cafe. → 전형적인 점심식사형 카테고리가 강세.

  • 오후틈(15–16시) : 전체 볼륨은 낮지만 chicken이 일찍부터 상승 조짐. kr·snack·cafe는 완만.

  • 저녁(17–20시) : chicken 압도적 1위, 다음 pig(고기)krsnack 순. 하루 최대 수요가 집중되는 메인 전장.

  • 야식(22–01시): chicken > pig > night(야식전용) 순으로 강세 지속. kr·snack은 중간권, pizza/jp/fast는 후순위.

  • 아침(07–10시): 전체량이 매우 작음. 그중 kr·fast·cafe가 상대적으로 상위지만 전반적 수요는 미미.

추후 분석

  • 평일 vs 주말×시간대 TOP-N 재계산 : 같은 그래프를 facet_grid(day_type ~ window)로 분리해 주말 패턴 차이 확인.

  • 비율(점유율) 기준으로 재비교 : 시간대별 총량이 다른 문제를 보정해 인기 ’구도’의 순수 변화를 보기.

  • 계절/날씨 영향 : 여름/겨울, 비/폭염/한파일에 시간대별 TOP-N이 어떻게 바뀌는지 (특히 저녁·야식의 치킨/고기 민감도).

  • 명절/공휴일 시간대 패턴 : 일반 공휴일 vs 설·추석에서 시간대 피크 시점/크기 변동(당일 저점, 전날 보상 상승 여부).

  • 스파이크 탐색(평일) : 각 시간대에서 이상급등일을 추출→ 날씨·프로모션·월급일과 대조해 원인 분해.

  • 메뉴 교차탄성 : 시간대별로 chicken 상승 시 pizza/fast대체/보완 관계인지 상관/회귀로 확인.

HUM(1)

# 0) 패키지
suppressPackageStartupMessages({
  library(showtext); library(sysfonts); library(systemfonts); library(ggplot2)
})

# 1) 'noto' 별칭 등록 (온라인 → 오프라인 폴백)
ok <- FALSE
try({
  showtext::font_add_google("Noto Sans KR", family = "noto")
  ok <- TRUE
}, silent = TRUE)

if (!ok) {
  # 시스템에 있는 한글 폰트 중 사용 가능 폰트 탐색 후 'noto' 별칭으로 바인딩
  cand <- c("Noto Sans KR","Apple SD Gothic Neo","Malgun Gothic","NanumGothic")
  fams <- unique(systemfonts::system_fonts()$family)
  pick <- intersect(cand, fams)[1]
  if (!is.na(pick)) {
    sysfonts::font_add(family = "noto", regular = systemfonts::match_font(pick)$path)
    ok <- TRUE
  }
}

showtext::showtext_auto(TRUE)

# 2) 전역 테마에 폰트 적용 (+ 텍스트 지오메트리 기본값도 세팅)
ggplot2::theme_set(theme_minimal(base_size = 11, base_family = "noto"))
ggplot2::update_geom_defaults("text",  list(family = "noto"))
ggplot2::update_geom_defaults("label", list(family = "noto"))

suppressPackageStartupMessages({
  library(dplyr); library(lubridate); library(ggplot2); library(scales); library(tidyr)
})

# 1) 메뉴 합계 만들기 + 시간 정리
menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")

d2 <- df %>%
  mutate(
    date  = as.Date(date),
    hour  = pmax(0, pmin(23, as.integer(time))),
    total_orders = rowSums(across(all_of(menu_cols)), na.rm = TRUE)
  )

# 2) 습도 3단계 구간화 (기본: 3분위수로 공정 분할)
qs <- quantile(d2$hum, probs = c(0, 1/3, 2/3, 1), na.rm = TRUE)
if (length(unique(qs)) < 4) {   # 혹시 동일값 문제 예방
  qs <- quantile(d2$hum, probs = c(0, .4, .7, 1), na.rm = TRUE)
}
d2 <- d2 %>%
  mutate(
    hum_level = cut(hum, breaks = qs, include.lowest = TRUE,
                    labels = c("건조","보통","습함"), right = TRUE),
    hum_level = factor(hum_level, levels = c("건조","보통","습함"), ordered = TRUE)
  )

# (선택) 고정 임계값 버전: 40% / 70% 기준
# d2 <- d2 %>% mutate(hum_level = cut(hum, breaks = c(-Inf, 40, 70, Inf),
#                                     labels = c("건조","보통","습함"), right = FALSE))


# ─────────────────────────────
# A) 습도 수준별 평균 주문량 비교(±95% CI)
# ─────────────────────────────
sum_by_level <- d2 %>%
  group_by(hum_level) %>%
  summarise(n = n(),
            mean_orders = mean(total_orders),
            se = sd(total_orders)/sqrt(n()),
            .groups = "drop")

p_bar <- ggplot(sum_by_level, aes(x = hum_level, y = mean_orders, fill = hum_level)) +
  geom_col(width = 0.65, show.legend = FALSE) +
  geom_errorbar(aes(ymin = mean_orders - 1.96*se,
                    ymax = mean_orders + 1.96*se), width = 0.18) +
  labs(title = "습도 수준별 평균 주문량 (±95% CI)",
       x = NULL, y = "평균 주문량") +
  theme_minimal(base_size = 11) +
  theme(panel.grid.minor = element_blank())
print(p_bar)

# ─────────────────────────────
# (옵션) 표본/레벨 차이 보정: '일내 비중(%)' 기준 비교
# ─────────────────────────────
share_by_level <- d2 %>%
  group_by(date, hour, hum_level) %>%
  summarise(total = sum(total_orders), .groups = "drop") %>%
  group_by(date) %>%
  mutate(day_sum = sum(total)) %>%
  filter(day_sum > 0) %>%
  mutate(share = total / day_sum) %>%
  group_by(hum_level, hour) %>%
  summarise(mean_share = mean(share), .groups = "drop")

p_share <- ggplot(share_by_level, aes(hour, mean_share, color = hum_level)) +
  geom_line(linewidth = 0.9) +
  scale_x_continuous(breaks = 0:23) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  labs(title = "시간대별 주문 비중(%) — 습도 수준 비교",
       x = "시간", y = "평균 비중(%)", color = "습도") +
  theme_minimal(base_size = 11)
# print(p_share)

요약

  • 정량 감(막대그래프): 평균 주문량은 건조 < 보통 < 습함 순이며, 습한 날이 건조한 날보다 약 2배 내외로 높게 보입니다(±95% CI도 거의 겹치지 않아 차이가 유의할 가능성 높음).
# ─────────────────────────────
# B) 시간대(0–23시) 패턴: 습도 수준 비교
# ─────────────────────────────
hourly_by_level <- d2 %>%
  group_by(hum_level, hour) %>%
  summarise(mean_orders = mean(total_orders), .groups = "drop")

p_line <- ggplot(hourly_by_level, aes(x = hour, y = mean_orders, color = hum_level)) +
  geom_line(linewidth = 0.9) +
  geom_point(size = 1.5) +
  scale_x_continuous(breaks = 0:23) +
  labs(title = "시간대별 주문 패턴 — 습도 수준 비교",
       x = "시간", y = "평균 주문량", color = "습도") +
  theme_minimal(base_size = 11) +
  theme(panel.grid.minor = element_blank())
print(p_line)

요약

  • 일관된 곡선: 모든 습도 수준에서 점심(11–12시대) 급상승 → 오후(14–16시) 완만 하락 → 저녁(18–19시) 최대 피크 → 야간 급락 형태는 동일합니다.

  • 레벨 차이(증폭 효과): 습함 > 보통 > 건조하루 전 구간에서 일관됩니다. 곡선이 위로 평행 이동한 듯한 배율(멀티플라이어) 효과에 가깝습니다.

  • 저녁 피크에서 격차 최대: 18–19시 구간에서 습한 날의 주문량이 가장 크게 벌어집니다(그래프상 습함 곡선이 가장 높고 건조가 가장 낮음).

## 월별 하루 평균 습도 변화 ##

suppressPackageStartupMessages({
  library(dplyr); library(lubridate); library(ggplot2); library(scales)
})

menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")

# 시간→일→주 요약: 주문가중 습도 & 주문량
hr <- df %>%
  mutate(date = as.Date(date),
         hour = pmax(0, pmin(23, as.integer(time))),
         orders = rowSums(across(all_of(menu_cols)), na.rm = TRUE)) %>%
  select(date, hour, hum, orders)

weekly <- hr %>%
  group_by(week = floor_date(date, "week", week_start = 1)) %>%
  summarise(
    hum_w = weighted.mean(hum, w = orders, na.rm = TRUE),   # 주문가중 주평균 습도
    ord   = sum(orders, na.rm = TRUE),                      # 주 주문량 합
    .groups = "drop"
  )

# 이중축용 '강건 스케일' (5~95 분위수로 매핑)
h_lo <- quantile(weekly$hum_w, .05, na.rm = TRUE)
h_hi <- quantile(weekly$hum_w, .95, na.rm = TRUE)
o_lo <- quantile(weekly$ord,  .05, na.rm = TRUE)
o_hi <- quantile(weekly$ord,  .95, na.rm = TRUE)
sf   <- (h_hi - h_lo) / (o_hi - o_lo)   # scale factor
off  <- h_lo - o_lo * sf                # offset

weekly <- weekly %>%
  mutate(ord_scaled = ord * sf + off)

ggplot(weekly, aes(week)) +
  # 습도(진한 초록)
  geom_line(aes(y = hum_w, color = "주문가중 습도"), linewidth = 1.1) +
  # 주문량(파랑) - 습도축에 매핑
  geom_line(aes(y = ord_scaled, color = "주문량(스케일링)"), linewidth = 0.9, alpha = 0.85) +
  scale_color_manual(values = c("주문가중 습도" = "#2ca02c", "주문량(스케일링)" = "#1f77b4"), name = NULL) +
  scale_y_continuous(
    name = "습도(%)",
    sec.axis = sec_axis(~ (. - off) / sf, name = "주문량(주 합계)",
                        labels = label_number(accuracy = 1, scale_cut = cut_short_scale()))
  ) +
  scale_x_date(date_breaks = "1 month", minor_breaks = "1 week", date_labels = "%Y-%m",
               expand = c(0.01, 0.01)) +
  labs(title = "주간 주문가중 습도 vs 주문량 (이중축, 강건 스케일)",
       subtitle = "X축: 월(주요 눈금)과 주(보조 눈금) · 이상치 완화(5–95 분위수 매핑)",
       x = NULL) +
  theme_minimal(base_size = 11) +
  theme(panel.grid.minor = element_line(linewidth = 0.2),
        axis.text.x = element_text(angle = 90, vjust = 0.5))

요약

  • 비조건부 시계열(주간 이중축): 시간의 흐름(계절·프로모션·추세)이 함께 섞여 보이므로, 습도 변화와 주문량이 항상 같이 움직이지는 않음. 즉, “습도 효과 + 다른 요인들”의 합으로 보인다고 해석하는 게 맞다.

추후 분석

  • 공정 비교용 지표 정착 : 주문가중 일평균 습도(전체), 저녁 가중 습도(17–20시) 두 개를 표준 지표로 운영.

  • 평일/주말 × 습도 3단계 재시각화 : 같은 시간대 그래프를 평일/주말로 분할해 “주말 증폭” 여부 확인.

  • 강수 분리 : rain > 0/=0으로 나눠 동일 그래프 생성 → 순수 습도 vs 비 효과를 눈으로 구분.

  • 메뉴 선호 교차 : (이미 만든 틀로) 습도×메뉴 점유율 히트맵/덤벨시간대별(점심/저녁/야식) 로 확장해 “습한 날 저녁에 특히 뜨는 메뉴”를 특정.

HUM(2)

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(ggplot2); library(scales)
})

# 0) 메뉴 열
menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")

# 1) 습도 3단계 구간화 (기본: 3분위 = 표본 균형)
d0 <- df %>% mutate(hum = as.numeric(hum))
qs <- quantile(d0$hum, probs = c(0, 1/3, 2/3, 1), na.rm = TRUE)
if (length(unique(qs)) < 4) qs <- quantile(d0$hum, probs = c(0, .4, .7, 1), na.rm = TRUE)

d1 <- d0 %>%
  mutate(hum_level = cut(hum, breaks = qs, include.lowest = TRUE,
                         labels = c("건조","보통","습함"), right = TRUE)) %>%
  # 메뉴 합계(행 단위 합) → 습도레벨×메뉴 합계
  mutate(total_row = rowSums(across(all_of(menu_cols)), na.rm = TRUE)) %>%
  select(hum_level, all_of(menu_cols))

agg <- d1 %>%
  group_by(hum_level) %>%
  summarise(across(all_of(menu_cols), ~sum(.x, na.rm = TRUE)), .groups = "drop") %>%
  pivot_longer(all_of(menu_cols), names_to = "menu", values_to = "orders") %>%
  group_by(hum_level) %>%
  mutate(share = orders / sum(orders)) %>%   # 각 습도레벨 내 점유율(%)
  ungroup()

# ─────────────────────────────────────────────
# A) 히트맵: 습도 × 메뉴 점유율(%)
#  - 메뉴 정렬: (습함 - 건조) 차이가 큰 순서로
# ─────────────────────────────────────────────
wide <- agg %>%
  select(hum_level, menu, share) %>%
  pivot_wider(names_from = hum_level, values_from = share)

wide2 <- wide %>%
  mutate(diff_humid_minus_dry = `습함` - `건조`)

menu_order <- wide2 %>% arrange(desc(diff_humid_minus_dry)) %>% pull(menu)

heat_dat <- agg %>%
  mutate(menu = factor(menu, levels = rev(menu_order)),
         hum_level = factor(hum_level, levels = c("건조","보통","습함"), ordered = TRUE))

p_heat <- ggplot(heat_dat, aes(x = hum_level, y = menu, fill = share)) +
  geom_tile() +
  scale_fill_viridis_c(labels = percent_format(accuracy = 1), name = "점유율") +
  labs(title = "습도 수준에 따른 메뉴 선호(점유율 %) — 히트맵",
       x = "습도 수준", y = NULL) +
  theme_minimal(base_size = 11) +
  theme(panel.grid = element_blank())
print(p_heat)

요약

  • 전반 패턴: 습도가 건조 → 보통 → 습함으로 높아질수록, 몇몇 메뉴의 점유율이 눈에 띄게 이동함(특히 치킨/고기 계열↑).

  • 습한 날 강세(↑) : chicken, pig(고기류): 습할수록 점유율 상승이 뚜렷. 저녁·야식 수요 증폭과도 잘 맞음.

  • kr(한식), steam(찜), night(야식전용) : 완만한 상승 경향.

  • 건조한 날 강세(↓) : cafe, fast, pizza, jp(일식), boxed는 습할수록 비중이 소폭 하락. (가벼운 간식/패스트푸드·디저트류가 상대적으로 밀림)

  • 대체로 변화 작음 : snack, ch(중식) 등은 큰 이동 없이 보합(소폭 변동 수준).

  • 해석 포인트: 이 결과는 “상대 비중(점유율)” 변화이므로, 전체 주문량이 늘어난 상황에서도 메뉴 간 판도가 어떻게 바뀌는지를 보여준다. 습한 날엔 ‘묵직·따뜻·단체 공유’ 성격(치킨·고기·찜)으로 기우는 경향이 있다.

추후 분석

  • 절대 주문량 버전의 동일 그래프 추가 : 점유율(%)과 건수(합계) 를 함께 보면 “판도 변화”와 “규모 변화”를 동시에 파악 가능.

  • 시간대 패싯(점심/저녁/야식 × 습도 3단계) : “습한 날 저녁에 특히 치킨·고기 쏠림이 강한가?”를 직접 비교.

  • 비 영향 분리 : rain>0 vs =0으로 나눠 재시각화 → 순수 습도 효과와 비 효과 구분.

  • 평일/주말 분리 : 주말엔 외출/단체 주문이 늘어 치킨·고기 쏠림이 더 커지는지 확인.

RAIN(1)

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales); library(forcats)
})

# ---- 0) 준비: 메뉴합계/구분/강수구간/월 ----
all_menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
                   "asian_western","ch","pig","night","steam","boxed","fast")
menu_cols <- intersect(all_menu_cols, names(df))

df0 <- df %>%
  mutate(
    date = as.Date(date),
    hour = pmax(0, pmin(23, as.integer(time))),
    wknd = if_else(lubridate::wday(date, week_start = 1) %in% c(6,7), "주말", "평일"),
    total = if ("total_orders" %in% names(.)) total_orders
            else rowSums(across(all_of(menu_cols)), na.rm = TRUE),
    rain_bin = cut(rain, breaks = c(-Inf, 0, 2, 8, Inf),
                   labels = c("0mm(건조)", "0–2mm", "2–8mm", "8mm+"), right = TRUE),
    month = floor_date(date, "month")
  ) %>%
  filter(!is.na(rain_bin))

# ---- 1) 기준선(월×주말/평일×시간) 계산 ----
base_mu <- df0 %>%
  group_by(month, wknd, hour) %>%
  summarise(baseline = mean(total), .groups = "drop")

dfb <- df0 %>%
  left_join(base_mu, by = c("month","wknd","hour")) %>%
  mutate(resid = total - baseline)

# ---- 2) 시간 버킷(6구간) 정의 ----
bucket6 <- function(h) dplyr::case_when(
  h %in% 0:5   ~ "새벽(00–05)",
  h %in% 6:10  ~ "아침(06–10)",
  h %in% 11:14 ~ "점심(11–14)",
  h %in% 15:16 ~ "오후(15–16)",
  h %in% 17:20 ~ "저녁(17–20)",
  h %in% 21:23 ~ "야간(21–23)",
  TRUE ~ NA_character_
)

# ---- 3) 하루×구간 단위로 합계 후, 구간 평균/SE ----
daily_bucket <- dfb %>%
  mutate(bucket = bucket6(hour)) %>%
  filter(!is.na(bucket)) %>%
  group_by(date, month, wknd, rain_bin, bucket) %>%
  summarise(resid_day = sum(resid), .groups = "drop")

# 구간별 일수(n)와 평균/SE
sum_bucket <- daily_bucket %>%
  group_by(wknd, rain_bin, bucket) %>%
  summarise(
    n_days   = n_distinct(date),
    mean_diff = mean(resid_day),
    se        = sd(resid_day)/sqrt(n_days),
    .groups = "drop"
  )

# ---- 4) 희귀 필터: 일반 n≥12, 폭우(8mm+) n≥3 ----
min_n_general <- 12
min_n_heavy   <- 3

sum_bucket_f <- sum_bucket %>%
  mutate(keep = if_else(rain_bin == "8mm+", n_days >= min_n_heavy,
                        n_days >= min_n_general)) %>%
  filter(keep)

##

p_heat <- ggplot(sum_bucket_f,
                 aes(x = wknd, y = bucket, fill = mean_diff)) +
  geom_tile(color = "white", linewidth = 0.4) +
  scale_fill_gradient2(low = "#2b8cbe", mid = "white", high = "#de2d26",
                       midpoint = 0, name = "증감(건수)") +
  geom_text(aes(label = paste0(ifelse(mean_diff>0,"+",""), round(mean_diff))),
            color = "gray20", size = 3) +
  facet_wrap(~ rain_bin, ncol = 2) +
  labs(title = "강수량×시간대 효과 히트맵(기준선 보정)",
       subtitle = "숫자=평균 증감(건수), 음수는 파랑·양수는 빨강",
       x = "구분", y = NULL) +
  theme_minimal(base_size = 13)

print(p_heat)

요약

  • 건조(0mm)

    • 주말·평일 모두 대체로 ±0 부근, 특히 저녁/야간은 소폭 음수(예: 평일 저녁 −26, 야간 −12).

    • 해석: 비가 올 때 늘어나는 수요가 기준선에 반영돼 있어서, 비가 안 오는 날은 상대적으로 낮게 보이는 효과.

  • 2) 약한 비(0–2mm) — 평일만 표시됨

    • 평일 전 시간대 전반 양수, 특히 저녁(17–20시): +126 (가장 큼)

    • 점심(11–14시): +48, 오후(15–16시): +40

    • 새벽/아침은 소폭 +3~+5

    • 해석: 평일에 약한 비가 오면 점심·저녁·오후 순으로 수요가 눈에 띄게 늘어남. “퇴근/식사 시간대” 효과가 분명.

  • 보통 비(2–8mm) — 평일 일부만 표시

    • 평일 야간(21–23시): +205 (매우 큼)

    • 아침 +6, 새벽 −2, 점심 −1 등 기타 시간대는 미미.

    • 해석: 비가 좀 오는 날엔 평일 늦은 밤 주문이 크게 늘어나는 패턴. (주말은 표본 부족으로 미표시)

  • 폭우(8mm+) — 평일 일부만 표시, 표본 적음(주의)

    • 평일 저녁(17–20시): −357(큰 감소)

    • 평일 야간(21–23시): +55(소폭 증가)

    • 아침 −4, 점심 −10 소폭 음수.

    • 해석(조심스럽게): 폭우 땐 저녁 주문이 위축, 일부 수요가 늦은 밤으로 이월될 가능성. (표본이 적어 확정적 판단은 금물)

  • 한줄 결론

    • 평일 + 비(특히 약한/보통 비)점심·저녁·야간 주문이 뚜렷하게 증가.

    • 건조한 날엔 기준선 대비 저녁·야간이 상대적으로 낮게 보임(비가 올 때의 상승이 빠져서).

    • 폭우는(표본 적지만) 저녁 급감·야간 일부 보상 패턴이 시사됨.

추후 분석

  • 비 × 온도/습도 상호작용 : 같은 비라도 덥고 습한 날 vs 춥고 건조한 날의 메뉴 선호가 다를 수 있음.
    비구간 × 온도(저/중/고) × 습도(건조/보통/습함) 3중 상호작용 테이블(간단 버전은 2×2).
    → “여름 장맛비 = 치킨·돼지고기·패스트 ↑ / 겨울 비 = 국물·찜 ↑” 같은 시나리오 확인.

RAIN(2)

# ===================== 세팅 =====================
suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(stringr); library(purrr)
})

# ▼ 사용자 데이터프레임 이름: df (기존과 동일 가정)
#    필수 열: date, time, rain, (아래 menu_cols 각 메뉴 열)
menu_cols <- intersect(
  c("kr","snack","cafe","jp","fish","chicken","pizza",
    "asian_western","ch","pig","night","steam","boxed","fast"),
  names(df)
)

# ===================== 1) 전처리 =====================
time_bucket <- function(h){
  cut(h, breaks = c(-1,5,10,14,16,20,23),
      labels = c("새벽(00–05)","아침(06–10)","점심(11–14)",
                 "오후(15–16)","저녁(17–20)","야간(21–23)"))
}

df0 <- df %>%
  mutate(
    date = as.Date(date),
    hour = pmax(0, pmin(23, as.integer(time))),
    bucket = time_bucket(hour),
    month  = floor_date(date, "month"),
    wknd   = if_else(wday(date, week_start = 1) >= 6, "주말","평일"),
    rain_grp = cut(rain, breaks = c(-Inf,0,2,Inf),
                   labels = c("0mm(건조)","0–2mm","≥2mm"), right = TRUE),
    total = rowSums(across(all_of(menu_cols)), na.rm = TRUE)
  ) %>% filter(!is.na(bucket), !is.na(rain_grp))

# 메뉴 Long
dt <- df0 %>%
  select(date, month, wknd, bucket, rain_grp, all_of(menu_cols), total) %>%
  pivot_longer(all_of(menu_cols), names_to = "menu", values_to = "cnt")

# ===================== 2) 기준선(건조) 대비 Δ점유율 =====================
# (a) 건조한 날(0mm) 기준선: 같은 월·주말/평일·시간대에서의 평균 점유율
base_share <- dt %>%
  filter(rain_grp == "0mm(건조)") %>%
  group_by(month, wknd, bucket, menu) %>%
  summarise(base_pp = sum(cnt, na.rm=TRUE)/sum(total, na.rm=TRUE)*100,
            n_base  = n(), .groups="drop")

# (b) 비 구간(0–2mm, ≥2mm) 점유율
grp_share <- dt %>%
  filter(rain_grp != "0mm(건조)") %>%
  group_by(rain_grp, month, wknd, bucket, menu) %>%
  summarise(g_pp = sum(cnt, na.rm=TRUE)/sum(total, na.rm=TRUE)*100,
            n_g  = n(), .groups="drop")

# (c) 같은 month·wknd·bucket·menu 기준으로 Δpp = (비) - (건조)
delta_month <- grp_share %>%
  left_join(base_share, by=c("month","wknd","bucket","menu")) %>%
  mutate(delta_pp = g_pp - base_pp)

# (d) month 층을 평균(가중치=해당 층 표본수) → 조건별 요약
delta_sum <- delta_month %>%
  group_by(rain_grp, wknd, bucket, menu) %>%
  summarise(
    n = sum(n_g, na.rm=TRUE),
    delta_pp = weighted.mean(delta_pp, w = pmax(n_g,1), na.rm=TRUE),
    .groups = "drop"
  )

# ===================== 3) 부트스트랩 CI (간단·층화: month 기준) =====================
# 각 (rain_grp, wknd, bucket, menu)에서 month별 델타를 재표본추출해 평균
set.seed(123)
B <- 500

boot_ci <- delta_month %>%
  group_by(rain_grp, wknd, bucket, menu) %>%
  reframe({
    mm <- month
    dd <- delta_pp
    if(length(unique(mm)) < 2) {              # 월이 1개면 CI 축소
      lo <- hi <- mean(dd, na.rm=TRUE)
    } else {
      b <- replicate(B, {
        take <- sample(seq_along(dd), replace=TRUE)
        # 월 층화 근사: 같은 길이에서 무작위 가중 → 평균
        mean(dd[take], na.rm=TRUE)
      })
      lo <- quantile(b, 0.025, na.rm=TRUE)
      hi <- quantile(b, 0.975, na.rm=TRUE)
    }
    tibble(lo=as.numeric(lo), hi=as.numeric(hi))
  }) %>%
  ungroup()

delta_sum_ci <- delta_sum %>%
  left_join(boot_ci, by=c("rain_grp","wknd","bucket","menu"))

# 가독성 위한 필터(표본 적은 칸은 반투명)
min_n <- 12     # 희귀 조합 제외선
plot_df <- delta_sum_ci %>%
  mutate(
    keep = n >= min_n,
    alpha = if_else(keep, 1, 0.25),
    label = if_else(keep, sprintf("%+.1f", delta_pp), "")
  )

# 메뉴 순서(많이 보는 메뉴 위로)
menu_order <- c("chicken","pig","kr","fast","pizza","snack","cafe","jp",
                "night","steam","ch","asian_western","fish","boxed")
plot_df$menu <- factor(plot_df$menu, levels = menu_order)

# ===================== 4) 히트맵 그리기 =====================
ggplot(plot_df %>% filter(rain_grp != "0mm(건조)"),
       aes(x=bucket, y=menu, fill=delta_pp, alpha=alpha)) +
  geom_tile(color="white", size=0.3) +
  geom_text(aes(label=label), size=3.2, color="black") +
  facet_grid(rain_grp ~ wknd, scales="free_x") +
  scale_fill_gradient2(low="#2b8cbe", mid="white", high="#d7301f",
                       midpoint=0, limits=c(-20, 20),
                       name="Δ점유율(pp)\n(비−건조)") +
  scale_alpha_identity() +
  labs(
    title="강수량×시간대에 따른 메뉴 ‘인기’ 변화 — Δ점유율(pp)",
    subtitle="기준선=같은 월·주말/평일·시간대의 건조(0mm) 평균 점유율\n숫자=평균 Δpp(표본 n≥12만 진하게 표시), 반투명=표본 부족",
    x="시간대", y="메뉴"
  ) +
  theme_minimal(base_size=13) +
  theme(panel.grid=element_blank(),
        axis.title.y=element_text(margin=margin(r=8)),
        strip.text=element_text(face="bold"))
Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
ℹ Please use `linewidth` instead.

요약

  • 전제: 아래 변화치는 모두 같은 월·주말/평일·시간대를 고정한 뒤, 건조(0 mm) 대비 ‘메뉴 점유율(%) 변화’를 본 결과이므로, 총 주문량이 아니라 “비중”의 이동

  • 0–2 mm(약한 비)

  • 평일 점심(11–14시): 카페/음료 비중↓, 따뜻한 식사(생선·한식·피자 등)로 소폭 분산↑.

    • 주말 점심: 피자·생선 비중↑, 패스트푸드 비중↓(대체로 +/– 수 pp 수준).

    • 저녁/야간(17–23시): 전반적으로 치킨 비중 약상승(완만).

    • 아침/오전: 변화 미미. → 해석: 가벼운 비에는 “점심 메뉴 구성이 바뀌는 정도(음료↓·식사↑)”, 저녁은 큰 폭 변화까지는 아님.

  • ≥2 mm(보통비~폭우; 2–8 mm와 8 mm+ 묶음)

    • 주말 저녁·야간(17–23시): 치킨 비중이 크게↑(히트맵 기준 +7~8 pp급). 다른 메뉴 비중은 상대적으로 하락.

    • 평일 저녁·야간: 치킨 비중 소폭↑, 카페/가벼운 메뉴는 약세.

    • 평일 점심: 카페/음료 비중↓ 경향이 더 뚜렷.

    • 새벽/오전: 방향성은 있지만 대체로 효과 작음.

    • 신뢰도 주의: ≥2 mm 구간은 일부 시간대·요일에서 표본이 적어(반투명 칸), 방향만 참고해줘.

  • 한 줄 요약(강수량별)

    • 약한 비(0–2 mm)점심에서 음료↓·식사↑로 구성 이동, 저녁은 완만한 치킨 선호.

    • 비가 제법 오면(≥2 mm)저녁·야간치킨 쏠림이 크다, 평일 점심 음료 비중은 더 줄어듦.

TYPE(1)

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate); library(ggplot2); library(scales)
})

# 0) 총주문량 만들기
menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")

d0 <- df %>%
  mutate(
    date  = as.Date(date),
    hour  = pmax(0, pmin(23, as.integer(time))),
    dow   = wday(date, week_start = 1),                 # 1=월 … 7=일
    total = rowSums(across(all_of(menu_cols)), na.rm = TRUE)
  ) %>%
  filter(!is.na(type))

# A) “일 내 비중(%)” — 내가 ‘주로’ 어떤 날씨에 주문하는지 (가장 직관적·공정)
by_day_type <- d0 %>%
  group_by(date, type) %>%
  summarise(orders = sum(total), .groups = "drop") %>%
  complete(date, type, fill = list(orders = 0)) %>%       # 그날 그 날씨가 없으면 0
  group_by(date) %>%
  mutate(day_sum = sum(orders)) %>%
  ungroup() %>%
  filter(day_sum > 0) %>%
  mutate(share = orders / day_sum)

type_share <- by_day_type %>%
  group_by(type) %>%
  summarise(
    days = n_distinct(date),
    mean_share = mean(share),
    se = sd(share)/sqrt(days),
    .groups = "drop"
  ) %>%
  mutate(type = reorder(type, mean_share, decreasing = TRUE))

p_contrib <- ggplot(type_share, aes(type, mean_share, fill = type)) +
  geom_col(width = 0.7, show.legend = FALSE) +
  geom_errorbar(aes(ymin = mean_share - 1.96*se, ymax = mean_share + 1.96*se),
                width = 0.15) +
  geom_text(aes(label = paste0("n=", days)), vjust = -0.6, size = 3) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  labs(title = "날씨별 ‘일 내 비중(%)’ — 주로 주문하는 날씨",
       subtitle = "각 날짜를 100%로 표준화 후, 모든 날짜를 동일 가중(±95% CI)",
       x = NULL, y = "평균 비중(%)") +
  theme_minimal(base_size = 11) +
  theme(panel.grid.minor = element_blank())
print(p_contrib)

요약

[막대그래프] 주로 주문한 날씨

  • 흐림이 압도적(≈55%), 그다음 보통(≈33%).

  • 맑음·비는 각각 **한 자릿수대(≈5–7%)**로 상대적으로 작음.

  • 진눈개비·눈거의 없음(≈0%).

  • 에러바(±95% CI)가 작아 결과가 안정적이라는 뜻.

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales)
})

# 0) 준비 -----------------------------------------------------------
menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")
min_days <- 20   # 드문 날씨 제외 기준(필요 시 10~30 사이로 조정)

# 1) 전처리: 총주문/주말·평일/월/시간/날씨 --------------------------
d0 <- df %>%
  mutate(
    date  = as.Date(date),
    hour  = pmax(0, pmin(23, as.integer(time))),
    dow   = wday(date, week_start = 1),
    wknd  = if_else(dow %in% c(6,7), "주말", "평일"),
    wx    = trimws(as.character(type)),
    total = if ("total_orders" %in% names(df)) total_orders
            else rowSums(across(all_of(menu_cols)), na.rm = TRUE),
    month = floor_date(date, "month")
  ) %>%
  filter(!is.na(wx), wx != "")

# 2) 기준선: (월×주말/평일×시간) 평균 주문량 -------------------------
base <- d0 %>%
  group_by(month, wknd, hour) %>%
  summarise(base_mean = mean(total), .groups = "drop")

# 3) 각 (월×주말/평일×시간×날짜×날씨)의 실제 주문량과 기준선 차이 ----
by_slot <- d0 %>%
  group_by(month, wknd, hour, wx, date) %>%
  summarise(orders = sum(total), .groups = "drop") %>%
  left_join(base, by = c("month","wknd","hour")) %>%
  mutate(
    diff_abs = orders - base_mean,                            # 절대 증가량(건수)
    diff_rel = (orders - base_mean) / pmax(base_mean, 1e-9)   # 상대 증가율
  )

# 4) 월별 표본 불균형 보정 가중치(IPW) -----------------------------
w_tbl <- by_slot %>%
  group_by(wknd, month, wx) %>% summarise(n_days = n_distinct(date), .groups="drop") %>%
  group_by(wknd, month) %>% mutate(target = mean(n_days, na.rm=TRUE),
                                   w = ifelse(n_days > 0, target/n_days, 0)) %>%
  ungroup() %>% select(wknd, month, wx, w, n_days)

by_slot_w <- by_slot %>% left_join(w_tbl, by = c("wknd","month","wx"))

# 5) 날씨별 ‘배달량 증가’ 요약(주말/평일별) -------------------------
uplift <- by_slot_w %>%
  group_by(wknd, wx) %>%
  summarise(
    slots      = n(),                                   # 사용된 (날짜×시간) 관측수
    days       = n_distinct(date),
    abs_uplift = sum(diff_abs * w, na.rm=TRUE) / sum(w, na.rm=TRUE),      # ↑건수
    rel_uplift = 100 * sum(diff_rel * w, na.rm=TRUE) / sum(w, na.rm=TRUE),# ↑%
    se_abs     = sd(diff_abs, na.rm=TRUE) / sqrt(slots),
    se_rel     = 100 * sd(diff_rel, na.rm=TRUE) / sqrt(slots),
    .groups = "drop"
  ) %>%
  filter(days >= min_days) %>%                                  # 희귀 날씨 제외
  arrange(wknd, desc(abs_uplift))

# 6A) 그래프: ‘증가율(%)’ – 무엇이 더 잘 팔리나(비율 기준) ----------
p_rel <- ggplot(uplift, aes(x = reorder(wx, rel_uplift), y = rel_uplift, fill = wknd)) +
  geom_col(position = position_dodge(width = 0.7), width = 0.65) +
  geom_errorbar(aes(ymin = rel_uplift - 1.96*se_rel, ymax = rel_uplift + 1.96*se_rel),
                position = position_dodge(width = 0.7), width = 0.2) +
  coord_flip() +
  scale_y_continuous(labels = percent_format(scale = 1, accuracy = 1)) +
  labs(title = "날씨별 배달량 증가율(기준: 월×주말/평일×시간 보정)",
       subtitle = paste0("희귀 날씨 제외: days ≥ ", min_days, " · 막대=평균, 에러바=±95% CI"),
       x = "날씨", y = "증가율(%)", fill = "구분") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom")


# 7) 결과 확인 -------------------------------------------------------
print(uplift)  # 표로도 확인(주말/평일 × 날씨별 ↑건수/↑%)
# A tibble: 8 × 8
  wknd  wx    slots  days abs_uplift rel_uplift se_abs se_rel
  <chr> <chr> <int> <int>      <dbl>      <dbl>  <dbl>  <dbl>
1 주말  흐림   1265    96      518.       251.   13.1    4.32
2 주말  비      144    24      298.       173.   31.7   13.6 
3 주말  보통   1297   100      252.       169.   10.3    4.62
4 주말  맑음    448    60       50.6       68.4  11.7    7.44
5 평일  흐림   2856   234      372.       239.    7.42   3.21
6 평일  비      462    73      265.       197.   18.0    8.56
7 평일  보통   3281   248      222.       177.    5.64   2.92
8 평일  맑음   1517   164       70.0       89.7   5.08   3.92
p_rel

요약

[막대그래프] 날씨별 배달량 증가율(%)

  • 흐림 > 비 > 보통 > 맑음 순으로 많이 는다.

  • 특히 평일주말보다 날씨 영향(증가율)이 더 큼.

  • 그래서: 흐리거나 비 오는 날 = 평일에 수요 급증이라고 보면 된다.

  • 맑은 날은 “날씨발 추가수요”가 가장 작다.

  • (참고) 눈/진눈개비처럼 드문 날씨는 제외되어 있음.

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales)
})

# ── 0) 준비 ─────────────────────────────────────────────────────────
menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")
min_days <- 20  # (wx×hour×wknd) 최소 날짜 수 필터 (10~30에서 조정)

d <- df %>%
  mutate(
    date  = as.Date(date),
    hour  = pmax(0, pmin(23, as.integer(time))),
    dow   = wday(date, week_start = 1),            # 1=월 … 7=일
    wknd  = if_else(dow %in% c(6,7), "주말", "평일"),
    wx    = trimws(as.character(type)),
    total = if ("total_orders" %in% names(df)) total_orders
            else rowSums(across(all_of(menu_cols)), na.rm = TRUE),
    month = floor_date(date, "month")
  ) %>%
  filter(!is.na(wx), wx != "")

# 각 날짜-시간-날씨-주말/평일 단위 주문 합
by_day_hr <- d %>%
  group_by(month, wknd, wx, date, hour) %>%
  summarise(orders = sum(total), .groups = "drop")

# ── A) 분포(%) 뷰: 하루=100% 표준화 + 월 가중치(IPW) ────────────────
shares <- by_day_hr %>%
  group_by(month, wknd, wx, date) %>%
  mutate(day_sum = sum(orders),
         share   = ifelse(day_sum > 0, orders/day_sum, NA_real_)) %>%
  ungroup() %>% filter(!is.na(share))

# 월별 표본 불균형 보정 가중치 (wx별 월-일수 균형)
w_tbl <- shares %>%
  group_by(wknd, month, wx) %>%
  summarise(n_days = n_distinct(date), .groups="drop") %>%
  group_by(wknd, month) %>%
  mutate(target = mean(n_days, na.rm=TRUE),
         w = ifelse(n_days > 0, target/n_days, 0)) %>%
  ungroup() %>%
  select(wknd, month, wx, w)

shape_sum <- shares %>%
  left_join(w_tbl, by = c("wknd","month","wx")) %>%
  group_by(wknd, wx, hour) %>%
  summarise(
    n_days = n_distinct(date),
    mean_s = sum(share * w, na.rm=TRUE) / sum(w, na.rm=TRUE),  # 가중 평균(%)
    .groups="drop"
  ) %>%
  filter(n_days >= min_days)

p_shape <- ggplot(shape_sum, aes(hour, mean_s, color = wx)) +
  geom_line(linewidth = 1.1) + geom_point(size = 1.2) +
  facet_wrap(~ wknd, ncol = 2) +
  scale_x_continuous(breaks = seq(0,23,2)) +
  scale_y_continuous(labels = percent_format(accuracy = 1)) +
  labs(title = "날씨별 하루 내 주문 ‘분포(%)’ — 주말 vs 평일",
       subtitle = paste0("하루=100% 표준화 · 월 가중치(IPW) · n_days≥", min_days),
       x = "시간", y = "평균 비중(%)", color = "날씨") +
  theme_minimal(base_size = 12) +
  theme(panel.grid.minor = element_blank(), legend.position = "bottom")

# ── B) 수준(건수) 뷰: 기준선(월×wknd×시간 평균) 대비 편차(건수) ───────
base <- d %>%
  group_by(month, wknd, hour) %>%
  summarise(base_mean = mean(total), .groups="drop")

level_sum <- by_day_hr %>%
  left_join(base, by = c("month","wknd","hour")) %>%
  mutate(diff_abs = orders - base_mean) %>%
  left_join(w_tbl, by = c("wknd","month","wx")) %>%
  group_by(wknd, wx, hour) %>%
  summarise(
    n_days = n_distinct(date),
    mean_diff = sum(diff_abs * w, na.rm=TRUE) / sum(w, na.rm=TRUE),  # +면 ‘평소보다’ 더 많음
    .groups = "drop"
  ) %>%
  filter(n_days >= min_days)

# level_sum 이 이미 만들어져 있다고 가정
p_level <- ggplot(level_sum, aes(hour, mean_diff, color = wx)) +
  geom_hline(yintercept = 0, linetype = "dashed", linewidth = 0.3) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 1.2) +
  facet_wrap(~ wknd, ncol = 2) +
  scale_x_continuous(breaks = 0:23, minor_breaks = 0:23)+
  labs(
    title = "날씨별 시간대 주문 ‘수준(건수)’ — 기준선 대비 편차",
    subtitle = "기준선=같은 월·주말/평일·시간의 전체 평균(보정) · n_days 필터 적용",
    x = "시간", y = "평균 편차(건수)", color = "날씨"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    panel.grid.minor = element_blank(),
    legend.position = "bottom"
  )+ annotate("rect", xmin=11, xmax=14, ymin=-Inf, ymax=Inf, alpha=.05) +
  annotate("rect", xmin=17, xmax=20, ymin=-Inf, ymax=Inf, alpha=.05) +
  annotate("rect", xmin=22, xmax=24, ymin=-Inf, ymax=Inf, alpha=.05)

print(p_level)

[선그래프] 날씨별 시간대 증감

  • 공통: 저녁(17–20시) 에 날씨 영향이 가장 크게 나타난다.

  • 주말

    • 흐림: 점심·저녁에 크게↑, 특히 저녁에 많이 오른다.

    • 보통: 점심·저녁 살짝↑

    • 맑음: 영향 거의 없음(0 근처).

  • 평일

    • 흐림: 점심(11–13시)·저녁 둘 다 크게↑.

    • 맑음: 점심만 소폭↑, 저녁은 거의 변화 없음.

    • 새벽/오전(0–10시)은 날씨 영향이 작다

TYPE(2)

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales); library(forcats); library(ggrepel)
})

# ─────────────────────────────
# 0) 기본 설정
# ─────────────────────────────
stopifnot(exists("df"))

# 메뉴 열 (데이터에 없으면 자동 제외)
all_menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
                   "asian_western","ch","pig","night","steam","boxed","fast")
menu_cols <- intersect(all_menu_cols, names(df))
if (length(menu_cols) == 0) stop("메뉴 열을 찾지 못했습니다.")

bucket_levels <- c("아침(07–10)","점심(11–14)","오후(15–16)","저녁(17–20)","야식(22–01)")
bucket_hour <- function(h) dplyr::case_when(
  h %in%  7:10 ~ "아침(07–10)",
  h %in% 11:14 ~ "점심(11–14)",
  h %in% 15:16 ~ "오후(15–16)",
  h %in% 17:20 ~ "저녁(17–20)",
  h %in% c(22,23,0,1) ~ "야식(22–01)",
  TRUE ~ NA_character_
)

min_days <- 15   # 희귀 조합 제외 기준(필요시 10~30 사이 조정)
top_n    <- 8    # 상위 메뉴 개수

# ─────────────────────────────
# 1) 전처리 → long → (날짜×시간대×날씨) 100% 표준화
# ─────────────────────────────
d0 <- df %>%
  mutate(
    date   = as.Date(date),
    hour   = pmax(0, pmin(23, as.integer(time))),
    wx     = trimws(as.character(type)),
    bucket = bucket_hour(hour)
  ) %>%
  filter(!is.na(date), !is.na(bucket), !is.na(wx), wx != "")

long <- d0 %>%
  pivot_longer(all_of(menu_cols), names_to = "menu", values_to = "qty") %>%
  group_by(wx, date, bucket, menu) %>%
  summarise(qty = sum(qty, na.rm = TRUE), .groups = "drop") %>%
  group_by(wx, date, bucket) %>%
  mutate(day_total = sum(qty, na.rm = TRUE),
         share = ifelse(day_total > 0, qty/day_total, NA_real_)) %>%
  ungroup() %>%
  filter(!is.na(share))

# ─────────────────────────────
# 2) 전체 Bump Chart (날씨 무시, 시간대별 순위 변화) + 라벨
# ─────────────────────────────
overall_share <- long %>%
  group_by(bucket, menu) %>%
  summarise(mean_share = mean(share), n_days = n_distinct(date), .groups = "drop") %>%
  filter(n_days >= min_days)

top_menus <- overall_share %>%
  group_by(menu) %>%
  summarise(overall = mean(mean_share), .groups = "drop") %>%
  slice_max(overall, n = top_n) %>%
  pull(menu)

bump_overall <- overall_share %>%
  filter(menu %in% top_menus) %>%
  mutate(bucket_f = factor(bucket, levels = bucket_levels)) %>%
  group_by(bucket_f) %>%
  mutate(rank = rank(-mean_share, ties.method = "first")) %>%
  ungroup()

max_rank_overall <- max(bump_overall$rank, na.rm = TRUE)

p_bump_overall <- ggplot(bump_overall,
                         aes(x = bucket_f, y = rank, group = menu, color = menu)) +
  geom_line(linewidth = 1.2, alpha = .9) +
  geom_point(size = 2) +
  scale_y_reverse(breaks = 1:max_rank_overall, limits = c(max_rank_overall, 1)) +
  labs(title = "메뉴 순위 Bump Chart — 전체(일 표준화 평균 점유율)",
       subtitle = paste0("상위 ", top_n, "개 메뉴 · 희귀 조합 제외: n_days ≥ ", min_days),
       x = "시간대", y = "순위(1=최상위)", color = "메뉴") +
  theme_minimal(base_size = 12) +
  theme(panel.grid.minor = element_blank(),
        legend.position = "bottom")

# 오른쪽 끝 라벨
last_points_overall <- bump_overall %>%
  group_by(menu) %>%
  slice_max(order_by = as.integer(bucket_f), n = 1, with_ties = FALSE) %>%
  ungroup()

# ─────────────────────────────
# 3) 날씨별 Bump Chart (맑음/보통/비/흐림) + 라벨
# ─────────────────────────────
wx_share <- long %>%
  group_by(wx, bucket, menu) %>%
  summarise(mean_share = mean(share), n_days = n_distinct(date), .groups = "drop") %>%
  filter(n_days >= min_days)

wx_top <- wx_share %>%
  group_by(wx, menu) %>%
  summarise(overall = mean(mean_share), .groups = "drop") %>%
  group_by(wx) %>% slice_max(overall, n = top_n, with_ties = FALSE) %>% ungroup()

bump_wx <- wx_share %>%
  semi_join(wx_top, by = c("wx","menu")) %>%
  mutate(bucket_f = factor(bucket, levels = bucket_levels)) %>%
  group_by(wx, bucket_f) %>%
  mutate(rank = rank(-mean_share, ties.method = "first")) %>%
  ungroup()

p_bump_wx <- ggplot(bump_wx,
                    aes(x = bucket_f, y = rank, group = menu, color = menu)) +
  geom_line(linewidth = 1.1, alpha = .9) +
  geom_point(size = 1.8) +
  scale_y_reverse(breaks = 1:top_n, limits = c(top_n, 1)) +
  facet_wrap(~ wx, ncol = 2) +
  labs(title = "메뉴 순위 Bump Chart — 날씨별",
       subtitle = paste0("각 날씨 상위 ", top_n, "개 메뉴 · n_days ≥ ", min_days),
       x = "시간대", y = "순위(1=최상위)", color = "메뉴") +
  theme_minimal(base_size = 12) +
  theme(panel.grid.minor = element_blank(),
        legend.position = "bottom")

# 패싯용 오른쪽 끝 라벨
last_points_wx <- bump_wx %>%
  group_by(wx, menu) %>%
  slice_max(order_by = as.integer(bucket_f), n = 1, with_ties = FALSE) %>%
  ungroup()

p_bump_wx_labeled <- p_bump_wx +
  ggrepel::geom_text_repel(
    data = last_points_wx,
    aes(label = menu),
    nudge_x = 0.25, direction = "y", hjust = 0,
    segment.color = NA, size = 3, show.legend = FALSE, max.overlaps = Inf
  ) +
  coord_cartesian(clip = "off") +
  theme(plot.margin = margin(5.5, 40, 5.5, 5.5))

print(p_bump_wx_labeled)

요약

[범프차트] 날씨별 시간대 메뉴 선호

  • 맑음

    • 낮(아침·점심): 카페·분식·일식(jp) 같은 가벼운 메뉴가 상대적으로 상위.

    • 저녁·야식: 치킨 1위는 동일하지만, pig/fast의 순위 상승 폭은 제한적(흐림/비보다 약함).

  • 보통

    • 전반적으로 기본 패턴.

    • 점심kr/snack 상위, 저녁치킨 1위 + pig/fast가 2–4위권에 자주 위치.

    • 저녁·야식에서 pig/fast가 확 올라와 2–3위권을 자주 차지(치킨 1위 유지).

    • 점심kr(따뜻하고 든든한 메뉴) 이 한 단계 더 위로.

    • 카페는 낮 시간에도 상대적으로 순위가 내려가는 편.

  • 흐림

    • 비와 비슷하거나 더 강한 패턴.

    • 저녁·야식에서 치킨 + pig/fast 결합 강세가 가장 뚜렷.

    • 낮 시간에는 카페 순위가 맑음 대비 더 뒤로 밀리는 경향.

Additional analysis

명절형 vs 여가형

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(stringr); library(forcats); library(purrr)
  library(scales)
})

# 데이터 프레임 이름 가정: df
# 반드시 있어야 하는 열: date(YYYY-MM-DD), time(0-23), 각 메뉴 열
menu_cols <- intersect(
  c("kr","snack","cafe","jp","fish","chicken","pizza",
    "asian_western","ch","pig","night","steam","boxed","fast"),
  names(df)
)

df <- df %>%
  mutate(
    date = as.Date(date),
    hour = pmax(0, pmin(23, as.integer(time))),
    wknd = if_else(wday(date, week_start = 1) >= 6, "주말", "평일"),
    total = rowSums(across(all_of(menu_cols)), na.rm = TRUE),
    ym    = floor_date(date, "month")
  )

##

holi_tbl <- tribble(
  ~holiday,          ~date,        ~type,
  # ===== 2019 =====
  "광복절",          as.Date("2019-08-15"), "여가형",
  "추석연휴",        as.Date("2019-09-12"), "명절형",
  "추석",            as.Date("2019-09-13"), "명절형",
  "추석연휴",        as.Date("2019-09-14"), "명절형",
  "개천절",          as.Date("2019-10-03"), "여가형",
  "한글날",          as.Date("2019-10-09"), "여가형",
  "성탄절",          as.Date("2019-12-25"), "여가형",
  # ===== 2020 =====
  "신정",            as.Date("2020-01-01"), "명절형",
  "설연휴",          as.Date("2020-01-24"), "명절형",
  "설날",            as.Date("2020-01-25"), "명절형",
  "설연휴",          as.Date("2020-01-26"), "명절형",
  "대체공휴일(설)",  as.Date("2020-01-27"), "명절형",
  "삼일절",          as.Date("2020-03-01"), "여가형",
  "국회의원선거",    as.Date("2020-04-15"), "여가형",
  "부처님오신날",    as.Date("2020-04-30"), "여가형",
  "어린이날",        as.Date("2020-05-05"), "여가형",
  "현충일",          as.Date("2020-06-06"), "여가형"
) %>%
  mutate(type = factor(type, levels = c("명절형","여가형")))

##

# a) 일 단위 집계(총 주문)
daily <- df %>%
  group_by(date, ym, wknd) %>%
  summarise(total_day = sum(total), .groups = "drop")

# b) 메뉴 점유율(일 단위)
share_daily <- df %>%
  group_by(date, ym, wknd) %>%
  summarise(across(all_of(menu_cols), sum), .groups = "drop") %>%
  mutate(total_day = rowSums(across(all_of(menu_cols))), .after = wknd) %>%
  mutate(across(all_of(menu_cols), ~ .x / pmax(total_day, 1e-9)))  # 점유율(0~1)

# c) 공휴일 윈도우 조인(±1일)
win_tbl <- holi_tbl %>%
  crossing(h = -1:1) %>%              # -1/0/+1
  mutate(date = date + days(h))

daily_e <- daily %>%
  inner_join(win_tbl, by = "date")    # 공휴일 주변 날짜만 남김

share_e <- share_daily %>%
  inner_join(win_tbl, by = "date")

##

# 일 단위 기준선(같은 월×주말/평일, 이벤트 제외)
base_day <- daily %>%
  anti_join(win_tbl, by = "date") %>%
  group_by(ym, wknd) %>%
  summarise(mu = mean(total_day), .groups = "drop")

# 이벤트 대비 변화(절대/증가율) + 요약
vol_delta <- daily_e %>%
  left_join(base_day, by = c("ym","wknd")) %>%
  mutate(diff = total_day - mu,
         rel  = (total_day - mu) / pmax(mu, 1e-9)) %>%
  group_by(type, h) %>%
  summarise(n=n(),
            diff_mean = mean(diff), diff_se = sd(diff)/sqrt(n),
            rel_mean  = mean(rel),  rel_se  = sd(rel)/sqrt(n),
            .groups = "drop") %>%
  mutate(h_f = factor(h, levels=c(-1,0,1), labels=c("전날(-1)","당일(0)","다음날(+1)")))

# 그래프: 증가율(%) 이벤트 스터디
g_vol <- ggplot(vol_delta, aes(h_f, rel_mean*100, fill=type)) +
  geom_hline(yintercept = 0, linetype="dashed") +
  geom_col(position=position_dodge(width=.7), width=.6) +
  geom_errorbar(aes(ymin=(rel_mean-rel_se*1.96)*100,
                    ymax=(rel_mean+rel_se*1.96)*100),
                width=.15, position=position_dodge(width=.7)) +
  labs(title="공휴일 타입별 주문량 변화(±1일, 증가율 %)",
       subtitle="기준선=같은 월×주말/평일 평균(이벤트 제외)",
       x=NULL, y="증가율(%)", fill="공휴일 타입") +
  theme_minimal(base_size=13)
print(g_vol)

요약

공휴일 타입에 따른 주문량 변화

  • 여가형(어린이날·선거·부처님오신날 등)

    • 당일(0): 크게 증가. 그래프상 대략 +20%대의 플러스, 신뢰구간도 명절형보다 좁아 일관된 상승으로 보입니다.

    • 전날(-1): 소폭 증가(+ 한 자릿수 %).

    • 다음날(+1): 약한 되돌림(소폭 감소 또는 거의 보합).

  • 명절형(설·추석·신정 등 가족형)

    • 당일(0): 감소(한 자릿수~두 자릿수 마이너스, CI 넓어 변동성 큼).

    • 다음날(+1): 감소폭이 더 큼(그래프상 가장 큰 마이너스).

    • 전날(-1): 거의 변화 없거나 소폭 감소.

##

# 시간대 기준선(월×주말/평일×시간대, 이벤트 제외)
base_hour_share <- df %>%
  anti_join(win_tbl, by="date") %>%
  group_by(ym, wknd, hour) %>%
  summarise(across(all_of(menu_cols), ~mean(.x/ pmax(total,1e-9))),
            .groups="drop")

# 이벤트 날짜의 "시간대별 점유율"을 구해 같은 키로 기준선 매칭 → 일 평균(시간 가중 평균)
share_event_hour <- df %>%
  semi_join(win_tbl, by="date") %>%
  mutate(across(all_of(menu_cols), ~ .x / pmax(total,1e-9))) %>%
  select(date, ym, wknd, hour, all_of(menu_cols)) %>%
  left_join(win_tbl %>% select(date, type, h), by="date") %>%
  left_join(base_hour_share, by=c("ym","wknd","hour"),
            suffix=c("", "_base"))
Warning in left_join(., win_tbl %>% select(date, type, h), by = "date"): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 65 of `x` matches multiple rows in `y`.
ℹ Row 4 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
  "many-to-many"` to silence this warning.
# 시간가중 평균으로 Δ점유율(pp) 집계
share_delta <- share_event_hour %>%
  pivot_longer(all_of(menu_cols), names_to="menu", values_to="share") %>%
  pivot_longer(ends_with("_base"), names_to="menu_base", values_to="share_base") %>%
  filter(str_remove(menu_base, "_base")==menu) %>%
  group_by(date, ym, wknd, type, h, menu) %>%
  summarise(delta = mean( (share - share_base) * 100 ), .groups="drop") %>%  # pp
  group_by(type, h, menu) %>%
  summarise(n=n(), d_mean = mean(delta), d_se=sd(delta)/sqrt(n),
            .groups="drop") %>%
  mutate(h_f = factor(h, levels=c(-1,0,1), labels=c("전날(-1)","당일(0)","다음날(+1)")),
         menu = fct_reorder(menu, d_mean, .fun = max, .desc = TRUE))

# 히트맵: 메뉴×(전/당/다음날) with 타입 facet
g_share <- ggplot(share_delta, aes(h_f, menu, fill = d_mean)) +
  geom_tile(color="white", size=.2) +
  scale_fill_gradient2(low="#2b8cbe", mid="white", high="#de2d26",
                       midpoint=0, limits=c(-10,10),
                       name="Δ점유율(pp)") +
  facet_wrap(~ type, ncol=2) +
  labs(title="공휴일 타입별 메뉴 점유율 변화(±1일, pp)",
       subtitle="기준선=같은 월×주말/평일×시간대 평균(이벤트 제외) · 양수=비중↑/음수=비중↓",
       x=NULL, y=NULL) +
  theme_minimal(base_size=12) +
  theme(panel.grid=element_blank())
print(g_share)

메뉴 점유율(pp) 변화

  • 명절형

    • 피자: 전날·당일·다음날 비중 상승(빨강) — “여럿이 쉬운 메뉴” 선호가 드러납니다.

    • 일식(jp): 전·당·다음날 비중 하락(파랑) — 비교적 감소 일관성.

    • 그 외 다수 카테고리는 미미하거나 혼재(소폭 ±).

    • → 명절 구간엔 공유/간편 메뉴(피자·패스트류) 가 상대적으로 유리.

  • 여가형

    • 당일(0) 기준으로 치킨·피자·한식/분식 일부에 소폭 플러스, 일식·카페류는 소폭 마이너스 경향이 관찰됩니다(대부분 ±1~3pp 수준의 작은 이동).

    • 야외/나들이 후 간편 저녁 수요 느낌: 치킨·피자 쪽 비중이 살짝 올라가는 패턴.

평일 스파이크 원인 파악

suppressPackageStartupMessages({
  library(dplyr); library(lubridate); library(tidyr); library(stringr)
})

get_top_bottom_days <- function(dat,
                                k_top = 10, k_low = 10,
                                use_month_wknd_baseline = TRUE,
                                holidays = NULL) {
  # ---- 0) 가드: 반드시 data.frame에서 시작 ----
  dat <- as.data.frame(dat, stringsAsFactors = FALSE)
  if (!"date" %in% names(dat)) stop("입력에 'date' 열이 없습니다.")
  if (!"time" %in% names(dat)) stop("입력에 'time' 열이 없습니다.")
  if (!all(c("rain","tem","hum") %in% names(dat))) {
    stop("입력에 'rain','tem','hum' 열이 필요합니다.")
  }

  # 메뉴 열 자동 탐지 (없으면 total_orders가 있다고 가정)
  menu_candidates <- c("kr","snack","cafe","jp","fish","chicken","pizza",
                       "asian_western","ch","pig","night","steam","boxed","fast")
  menu_cols <- intersect(menu_candidates, names(dat))

  # ---- 1) 일 단위 집계 ----
  daily <- dat |>
    dplyr::mutate(
      date = as.Date(date),
      hour = as.integer(time),
      total_orders = if ("total_orders" %in% names(dat)) total_orders
                     else if (length(menu_cols) > 0)
                       rowSums(dplyr::across(dplyr::all_of(menu_cols)), na.rm = TRUE)
                     else stop("메뉴 열도 total_orders도 없습니다.")
    ) |>
    dplyr::group_by(date) |>
    dplyr::summarise(
      orders_day = sum(total_orders, na.rm = TRUE),
      rain_day   = sum(rain, na.rm = TRUE),
      tem_day    = mean(tem, na.rm = TRUE),
      hum_day    = mean(hum, na.rm = TRUE),
      .groups = "drop"
    ) |>
    dplyr::mutate(
      dow   = lubridate::wday(date, week_start = 1),
      wknd  = ifelse(dow >= 6, "주말", "평일"),
      ym    = lubridate::floor_date(date, "month"),
      dom   = lubridate::mday(date),
      is_pay_window = dplyr::between(dom, 20, 25)
    )

  # ---- 1-1) 날씨 type(모드) + 분포 요약 붙이기 ----
  if ("type" %in% names(dat)) {
    type_sum <- dat |>
      dplyr::mutate(date = as.Date(date),
                    type = as.character(type),
                    type = trimws(type)) |>
      dplyr::filter(!is.na(type), type != "") |>
      dplyr::count(date, type, name = "n_type") |>
      dplyr::group_by(date) |>
      dplyr::mutate(total = sum(n_type),
                    share = n_type / total) |>
      dplyr::arrange(date, dplyr::desc(share), type) |>
      dplyr::summarise(
        type_top       = dplyr::first(type),
        type_top_share = dplyr::first(share),
        type_mix       = paste0(type, " ", sprintf("%.0f%%", share*100), collapse = " | "),
        n_types        = dplyr::n(),
        .groups = "drop"
      )

    daily <- dplyr::left_join(daily, type_sum, by = "date")
  } else {
    daily <- daily |>
      dplyr::mutate(type_top = NA_character_,
                    type_top_share = NA_real_,
                    type_mix = NA_character_,
                    n_types = NA_integer_)
  }

  # 공휴일 태깅(선택)
  if (!is.null(holidays)) {
    holidays <- as.data.frame(holidays)
    if (!"date" %in% names(holidays)) stop("holidays에 'date' 열이 없습니다.")
    if (!"holiday_name" %in% names(holidays)) holidays$holiday_name <- ""
    holidays$date <- as.Date(holidays$date)
    daily <- dplyr::left_join(daily, holidays, by = "date") |>
      dplyr::mutate(holiday_name = tidyr::replace_na(holiday_name, ""))
  } else {
    daily$holiday_name <- ""
  }

  # ---- 2) 기준선 설정 ----
  if (use_month_wknd_baseline) {
    daily <- daily |>
      dplyr::group_by(ym, wknd) |>
      dplyr::mutate(baseline = mean(orders_day, na.rm = TRUE)) |>
      dplyr::ungroup() |>
      dplyr::mutate(base_type = "월×주말/평일 평균")
  } else {
    mu <- mean(daily$orders_day, na.rm = TRUE)
    daily <- daily |>
      dplyr::mutate(baseline = mu, base_type = "전기간 단순 평균")
  }
  daily <- dplyr::mutate(daily, delta = orders_day - baseline)

  # ---- 3) 태그(보기 좋게) ----
  rain_tag <- function(x) cut(x, breaks = c(-Inf,0,2,8,Inf),
                              labels = c("건조(0mm)","약한 비(0–2)","보통 비(2–8)","폭우(8+)"),
                              right = TRUE)
  q_tem_hi <- stats::quantile(daily$tem_day, 0.90, na.rm = TRUE)
  q_tem_lo <- stats::quantile(daily$tem_day, 0.10, na.rm = TRUE)
  q_hum_hi <- stats::quantile(daily$hum_day, 0.90, na.rm = TRUE)

  daily <- daily |>
    dplyr::mutate(
      rain_bin = rain_tag(rain_day),
      tem_tag  = dplyr::case_when(
        tem_day >= q_tem_hi ~ "고온(상위10%)",
        tem_day <= q_tem_lo ~ "저온(하위10%)",
        TRUE ~ "보통"
      ),
      hum_tag  = ifelse(hum_day >= q_hum_hi, "고습(상위10%)", "보통")
    )

  # ---- 4) 상위/하위 k일 뽑기 ----
  top_high <- daily |> dplyr::arrange(dplyr::desc(delta)) |> dplyr::slice_head(n = k_top)
  top_low  <- daily |> dplyr::arrange(delta)             |> dplyr::slice_head(n = k_low)

  fmt_date <- function(d) format(d, "%Y-%m-%d (%a)")
  out_cols <- c("date","wknd","orders_day","baseline","delta",
                "rain_day","tem_day","hum_day",
                "rain_bin","tem_tag","hum_tag",
                "type_top","type_top_share","type_mix","n_types",
                "is_pay_window","holiday_name","base_type")

  result_high <- top_high |>
    dplyr::select(dplyr::all_of(out_cols)) |>
    dplyr::mutate(date = fmt_date(date),
                  is_pay_window = ifelse(is_pay_window, "Y", ""),
                  type_top_share = round(type_top_share*100, 1))  # %

  result_low <- top_low |>
    dplyr::select(dplyr::all_of(out_cols)) |>
    dplyr::mutate(date = fmt_date(date),
                  is_pay_window = ifelse(is_pay_window, "Y", ""),
                  type_top_share = round(type_top_share*100, 1))  # %

  list(high = result_high, low = result_low, daily = daily)
}
res <- get_top_bottom_days(df, k_top = 10, k_low = 10, use_month_wknd_baseline = TRUE)

res$high
# A tibble: 10 × 18
   date       wknd  orders_day baseline  delta rain_day tem_day hum_day rain_bin
   <chr>      <chr>      <dbl>    <dbl>  <dbl>    <dbl>   <dbl>   <dbl> <fct>   
 1 2020-01-0… 평일       22800   12698. 10102.     83.5    4.23    89.3 폭우(8+)
 2 2019-12-1… 평일       21714   12419.  9295.      0.5    3.66    84.9 약한 비(0–…
 3 2019-12-3… 평일       18483   12419.  6064.      0     -5.83    53.5 건조(0mm)…
 4 2019-12-2… 평일       17021   12419.  4602.      1      3.04    68.9 약한 비(0–…
 5 2019-08-1… 평일       12606    8185.  4421.     65     24.4     95.0 폭우(8+)
 6 2020-05-1… 평일       18595   14338.  4257.     27     15.4     95.7 폭우(8+)
 7 2020-02-2… 평일       17408   13231.  4177.      8.5    4.75    91.7 폭우(8+)
 8 2019-11-1… 평일       14332   10414.  3918.     34      5.56    96.0 폭우(8+)
 9 2019-07-2… 평일        8930    5096.  3834.    102     25.3     95.7 폭우(8+)
10 2020-05-0… 평일       17906   14338.  3568.     83     16.8     73.0 폭우(8+)
# ℹ 9 more variables: tem_tag <chr>, hum_tag <chr>, type_top <chr>,
#   type_top_share <dbl>, type_mix <chr>, n_types <int>, is_pay_window <chr>,
#   holiday_name <chr>, base_type <chr>
res$low
# A tibble: 10 × 18
   date       wknd  orders_day baseline  delta rain_day tem_day hum_day rain_bin
   <chr>      <chr>      <dbl>    <dbl>  <dbl>    <dbl>   <dbl>   <dbl> <fct>   
 1 2019-12-0… 주말        6144   14741. -8597.      0     0.737    73.2 건조(0mm)…
 2 2020-01-2… 주말        8257   14639. -6382.      0     2.03     82.4 건조(0mm)…
 3 2019-09-0… 주말        6166   10136. -3970.     13.5  24.9      90.1 폭우(8+)
 4 2019-12-0… 평일        8997   12419. -3422.      0    -2.73     60.7 건조(0mm)…
 5 2020-05-0… 평일       11456   14338. -2882.      0    19.6      42.1 건조(0mm)…
 6 2019-07-1… 주말        4193    7025. -2832.      0    24.0      87.6 건조(0mm)…
 7 2019-12-1… 평일        9677   12419. -2742.     37.5   9.92     72.2 폭우(8+)
 8 2019-10-0… 주말       10062   12626. -2564.      0.5  15.9      78.4 약한 비(0–…
 9 2019-12-0… 평일       10053   12419. -2366.      5.5   1.51     76.0 보통 비(2–…
10 2019-10-0… 주말       10306   12626. -2320.     45    17.0      78.6 폭우(8+)
# ℹ 9 more variables: tem_tag <chr>, hum_tag <chr>, type_top <chr>,
#   type_top_share <dbl>, type_mix <chr>, n_types <int>, is_pay_window <chr>,
#   holiday_name <chr>, base_type <chr>

요약

급상승 TOP 10 원인 추정

  • 2020-01-06(월) Δ+10,102 | 폭우 83.5mm, 습도 89%
    → 새해 첫 업무주 월요일 + 폭우. 강력한 ‘실내화’ 요인.

  • 2019-12-17(화) Δ+9,295 | 약한 비, 습도 85%
    → 연말 성수기(회사·모임) + 궂은 날씨. 프로모션/사내행사 유무 확인 권장.

  • 2019-12-31(화) Δ+6,064 | 건조, 강한 한파(−5.8℃)
    연말 특수(집 모임/야식) + 한파 효과. 날씨가 건조여도 ’캘린더’가 스파이크 유발.

  • 2019-12-25(수) Δ+4,602 | 약한 비, 크리스마스(공휴일)
    명절형 공휴일 + 날씨 보조. ‘가벼운 외식→배달 대체’ 패턴 가능.

  • 2019-08-15(목) Δ+4,421 | 폭우 65mm, 광복절(공휴일), 습도 95%
    공휴일 + 폭우 시그널 뚜렷.

  • 2020-05-15(금) Δ+4,257 | 폭우 27mm, 스승의날(평일), 습도 96%
    기념일 + 금요일 + 폭우의 3중 효과.

  • 2020-02-28(금) Δ+4,177 | 폭우 8.5mm, 습도 91%
    장마/폭우 + 금요일 전형. (또한 2020년 2월은 코로나 초기로 외식회피도 가세했을 가능성)

  • 2019-11-15(금) Δ+3,918 | 폭우 34mm, 습도 96%
    → 금요일 + 폭우.

  • 2019-07-26(금) Δ+3,834 | 폭우 102mm(!), 습도 95%
    장마철 기록적 호우 + 금요일. 날씨 단독으로도 스파이크 가능.

  • 2020-05-08(금) Δ+3,568 | 폭우 83mm, 어버이날(평일)
    기념일 + 금요일 + 폭우 재현.

급상승 공통점

  • 기상 트리거: 폭우(≥8mm)와 고습이 핵심.

  • 달력 트리거: 연말/공휴일/기념일.

  • 요일 트리거: 금요일.

급하락 BOTTOM 10 원인 추정

  • 2019-12-01(일) 건조, delta –8,597
    월초 일요일 특이 저조. 대면외식/이동 증가 또는 파트너 휴무 영향 가능.

  • 2020-01-25(토) 건조, –6,382
    설날(명절형) 당일. 가족 모임/외식으로 배달 급감이 자연스러움.

  • 2019-09-07(토) 폭우(13.5mm), –3,970
    주말 강한 비 → 주문 위축(외출/물류 모두 부담).

  • 2019-12-02(월) 저온(하위10%), –3,422
    월초 월요일 한파 + 전일 부진 여파. 오프라인 재개/업장휴무 가능성.

  • 2020-05-06(수) 건조, –2,882
    어린이날(5/5) 다음날. 전날 수요 앞당김 후 반동(–).

  • 2019-07-13(토) 건조, –2,832
    한여름 토요일. 야외활동/여행 시즌의 여가형 주말로 배달 약세.

  • 2019-12-16(월) 폭우(37.5mm), –2,742
    평일이지만 폭우(8mm+) 수준이면 저녁 운송 차질로 순감 가능(앞선 결과와 합치).

  • 2019-10-05(토) 약한 비, –2,563
    개천절(10/3) 직후의 긴 연휴 주말. 외출·여행 쏠림 → 배달 약세.

  • 2019-12-03(화) 보통 비(5.5mm), –2,366
    월초 초반 연속 저조. 기상+캘린더 복합.

  • 2019-10-06(일) 폭우(45mm), –2,319
    주말 집중호우로 수요/공급 모두 위축.

급하강 공통점

  • 주말 + 폭우 효과 : 하위 10일 중 주말 6/10, 그중 폭우가 낀 주말에서 크게 빠짐.

  • 연휴 전·후 효과 : 명절 당일/직후(설, 어린이날 다음날, 연휴 주말) 같은 캘린더 이벤트 인접일이 다수.

  • 급하락일은 건조한 주말(외출/여행·외식 선호) 또는 주말 폭우로 공급·수요 모두 위축이 겹쳤음.

  • 월초 특정 구간(예: 12/1–12/3 연속 저조)처럼 월내 패턴도 존재.

시간대 × 요일/공휴일 교차 패턴

# ===========================
# 패키지
# ===========================
suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales); library(stringr)
})

# ===========================
# 헬퍼: 메뉴열 감지 / 공휴일 입력 정규화
# ===========================
.menu_cols <- function(nm) {
  intersect(
    c("kr","snack","cafe","jp","fish","chicken","pizza",
      "asian_western","ch","pig","night","steam","boxed","fast"),
    nm
  )
}

.normalize_holidays <- function(holidays) {
  # holidays가 NULL / 길이0 / data.frame / Date 벡터 모두 허용
  if (is.null(holidays)) return(NULL)

  if (inherits(holidays, "Date")) {
    if (length(holidays) == 0) return(NULL)
    return(data.frame(
      date = as.Date(holidays),
      holiday_name = rep("공휴일", length(holidays)),
      stringsAsFactors = FALSE
    ))
  }

  # data.frame인 경우
  hd <- as.data.frame(holidays, stringsAsFactors = FALSE)
  if (nrow(hd) == 0) return(NULL)
  if (!"date" %in% names(hd)) stop("holidays에 'date' 열이 필요합니다.")
  if (!"holiday_name" %in% names(hd)) hd$holiday_name <- "공휴일"
  hd$date <- as.Date(hd$date)
  hd
}

# ===========================
# 공통 전처리
# - total(총주문), 요일/주말, 공휴일 플래그, 요일+공휴일 팩터
# ===========================
.base_prep <- function(data, holidays = NULL){
  menu_cols <- .menu_cols(names(data))
  hd <- .normalize_holidays(holidays)

  d <- data %>%
    mutate(
      date  = as.Date(date),
      hour  = pmax(0, pmin(23, as.integer(time))),
      total = if ("total_orders" %in% names(.)) total_orders
              else if (length(menu_cols) > 0)
                rowSums(across(all_of(menu_cols)), na.rm = TRUE)
              else stop("total_orders 또는 메뉴별 열(kr, snack, ...)이 필요합니다."),
      dow   = wday(date, week_start = 1), # 1=월 ... 7=일
      wknd  = if_else(dow >= 6, "주말", "평일"),
      yoil  = factor(c("월","화","수","목","금","토","일")[dow],
                     levels = c("월","화","수","목","금","토","일"))
    )

  if (!is.null(hd)) {
    d <- d %>%
      left_join(hd, by = "date") %>%
      mutate(is_holiday = !is.na(holiday_name))
  } else {
    d <- d %>% mutate(is_holiday = FALSE, holiday_name = NA_character_)
  }

  d %>%
    mutate(
      yoil_h = factor(if_else(is_holiday, "공휴일", as.character(yoil)),
                      levels = c("월","화","수","목","금","토","일","공휴일"))
    )
}

# ============================================================
# 1) 시간대 × 요일/공휴일 교차 패턴 (하루 100% 표준화 히트맵)
#    - 각 날짜 합계를 100%로 만든 뒤 시간대 비중 평균
# ============================================================
plot_hour_x_daytype_heatmap <- function(df, holidays = NULL, min_days = 12){
  d <- .base_prep(df, holidays)

  daily_shape <- d %>%
    group_by(date) %>%
    mutate(day_sum = sum(total, na.rm = TRUE),
           share   = if_else(day_sum > 0, total/day_sum, 0)) %>%
    ungroup()

  heat <- daily_shape %>%
    group_by(yoil_h, hour) %>%
    summarise(n_days = n_distinct(date),
              mean_share = mean(share, na.rm = TRUE), .groups = "drop") %>%
    filter(n_days >= min_days)

  # y축 역순 보기 좋게
  heat <- heat %>%
    mutate(yoil_h_rev = factor(yoil_h, levels = rev(levels(yoil_h))))

  p <- ggplot(heat, aes(hour, yoil_h_rev, fill = mean_share)) +
    geom_tile() +
    geom_text(aes(label = percent(mean_share, accuracy = 1)),
              size = 2.8, color = "grey10") +
    scale_fill_viridis_c(labels = percent_format(accuracy = 1)) +
    scale_x_continuous(breaks = seq(0, 23, 2)) +
    labs(title    = "시간대 × 요일/공휴일 교차 패턴 (하루 100% 표준화)",
         subtitle = paste0("각 날짜를 100%로 표준화 후 평균 · 각 칸 n≥", min_days),
         x = "시간", y = "요일/공휴일", fill = "평균 비중") +
    theme_minimal(base_size = 12)

  print(p); invisible(p)
}

# ============================================================
# 2) 공휴일 유형별 시간 흐름에 따른 주문량 변화(기준선 보정)
#    기준선 = 같은 월×주말/평일×시간(휴일 제외) 평균
#    - 절대 물량 차이/계절/요일 영향 제거 후 '공휴일 효과'만 보기
# ============================================================
plot_holiday_type_hour_effect <- function(df, holidays,
                                          min_days_each = 3,
                                          show_ci = TRUE){
  # 공휴일이 반드시 필요
  hd <- .normalize_holidays(holidays)
  if (is.null(hd)) stop("공휴일 리스트(holidays)가 비어 있습니다.")

  d <- .base_prep(df, hd) %>%
    mutate(month = floor_date(date, "month"))

  # 공휴일 유형 분류(원하면 키워드 추가/수정)
  holi_keys_m <- c("설","추석","신정","한가위")
  holi_keys_y <- c("어린이날","부처님오신날","현충일","선거","광복절","개천절","성탄절")
  d <- d %>%
    mutate(holi_type = case_when(
      !is_holiday ~ NA_character_,
      str_detect(holiday_name, paste(holi_keys_m, collapse = "|")) ~ "명절형",
      str_detect(holiday_name, paste(holi_keys_y, collapse = "|")) ~ "여가형",
      TRUE ~ "기타"
    ))

  # 기준선(휴일 제외) 계산
  base_mu <- d %>%
    filter(!is_holiday) %>%
    group_by(month, wknd, hour) %>%
    summarise(baseline = mean(total, na.rm = TRUE), .groups = "drop")

  holi_hour <- d %>%
    filter(is_holiday & !is.na(holi_type)) %>%
    left_join(base_mu, by = c("month","wknd","hour")) %>%
    filter(!is.na(baseline)) %>%                            # 기준선 없는 조합 제거
    mutate(diff = total - baseline) %>%
    group_by(holi_type, hour, date) %>%                     # 하루 합(여러 레코드면 합산)
    summarise(diff = sum(diff, na.rm = TRUE), .groups = "drop") %>%
    group_by(holi_type, hour) %>%
    summarise(n_days = n_distinct(date),
              mean_diff = mean(diff, na.rm = TRUE),
              se = sd(diff, na.rm = TRUE)/sqrt(n_days),
              .groups = "drop") %>%
    filter(n_days >= min_days_each)

  if (nrow(holi_hour) == 0) stop("필터 후 남은 공휴일 관측이 없습니다. min_days_each를 낮춰보세요.")

  p <- ggplot(holi_hour, aes(hour, mean_diff, color = holi_type, group = holi_type)) +
    geom_hline(yintercept = 0, linetype = "dashed") +
    { if (show_ci)
        geom_ribbon(aes(ymin = mean_diff - 1.96*se, ymax = mean_diff + 1.96*se,
                        fill = holi_type, color = NULL), alpha = .12)
    } +
    geom_line(size = 1.1) + geom_point(size = .9) +
    scale_x_continuous(breaks = 0:23) +
    labs(title    = "공휴일 유형별 시간 흐름에 따른 주문량 변화(기준선 보정)",
         subtitle = paste0("기준선=같은 월×주말/평일×시간(휴일 제외) 평균 · 각 시각 n≥", min_days_each),
         x = "시간", y = "기준선 대비 편차(건수)", color = "공휴일 유형", fill = "공휴일 유형") +
    theme_minimal(base_size = 12)

  print(p); invisible(p)
}

# ===========================
# 사용 예
# ===========================
# 1) 히트맵 (공휴일 없어도 OK)
 p1 <- plot_hour_x_daytype_heatmap(df, holidays = NULL, min_days = 12)

요약

[히트맵] 시간에 따른 요일/공휴일 교차 패턴

  • 점심(11–13시)·저녁(17–20시) 두 봉우리가 전 요일 공통 패턴.

  • 금요일 저녁(18–20시) 비중 최대: 같은 날의 주문이 저녁 시간대로 더 몰림.

  • 주말(토·일) 점심 비중↑, 야간(21–23시) 비중↓: 평일 대비 “낮에 미리 먹고 늦게는 덜 시킨다” 쪽으로 모양이 이동.

  • 새벽(0–5시)은 모든 요일에서 비중이 매우 낮음(≈0%).

# 2) 공휴일 유형 그래프 (공휴일 필요)
 holidays_df <- data.frame(
   date = as.Date(c("2019-08-15","2019-09-12","2019-09-13","2019-09-14",
                    "2020-01-01","2020-01-24","2020-01-25","2020-01-26",
                    "2019-05-05","2019-05-12","2019-06-06","2019-12-25")),
   holiday_name = c("광복절","추석연휴","추석","추석연휴",
                    "신정","설연휴","설","설연휴",
                    "어린이날","부처님오신날","현충일","성탄절")
 )
p2 <- plot_holiday_type_hour_effect(df, holidays = holidays_df, min_days_each = 2, show_ci = TRUE)

[선그래프] 공휴일 유형별 시간 흐름에 따른 주문량 변화

  • 여가형(어린이날·부처님오신날·현충일 등)

    • 점심(11–13시) 대폭 증가 → +400~700건 수준 피크.

    • 이른 저녁(16–19시)도 추가 상승 후 20시 이후 빠르게 정상화.

    • 요지는 “낮 중심의 외출/여가 동선 + 가족 단위 배달 수요”가 강함.

  • 명절형(설·추석·신정 등)

    • 점심·저녁 모두 감소(특히 17–19시 하락 폭 큼), 20시 이후에도 약한 마이너스.

    • 가정 내 대면 식사·이동으로 배달 대체가 줄어드는 전형적 패턴.

점심/저녁/야식 × 습도 3단계

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales); library(forcats); library(ggrepel)
})

# 전역 테마: 기본 폰트(경고 없음)
theme_set(theme_minimal(base_size = 11))

# ─────────────────────────────────────────────
# 0) 메뉴 열 정의
# ─────────────────────────────────────────────
menu_cols <- c("kr","snack","cafe","jp","fish","chicken","pizza",
               "asian_western","ch","pig","night","steam","boxed","fast")

# ─────────────────────────────────────────────
# D) 습도 3분위 × 시간대별 메뉴 점유율(%) ±95% CI — 전체 메뉴
#     (행=습도, 열=시간대, coord_flip으로 가독성 강화)
# ─────────────────────────────────────────────
plot_humidity_menu_share_all <- function(df, min_days = 12) {

  menu_cols <- intersect(
    c("kr","snack","cafe","jp","fish","chicken","pizza",
      "asian_western","ch","pig","night","steam","boxed","fast"),
    names(df)
  )
  stopifnot(length(menu_cols) > 0)

  d0 <- df %>%
    mutate(
      date  = as.Date(date),
      hour  = pmax(0, pmin(23, as.integer(time))),
      period = case_when(
        hour %in% 11:14 ~ "점심(11–14)",
        hour %in% 17:20 ~ "저녁(17–20)",
        hour %in% 21:23 ~ "야식(21–23)",
        TRUE ~ NA_character_
      )
    ) %>%
    filter(!is.na(period))

  hum_day <- d0 %>%
    group_by(date) %>%
    summarise(hum_day = mean(hum, na.rm = TRUE), .groups = "drop")

  qs <- quantile(hum_day$hum_day, probs = c(1/3, 2/3), na.rm = TRUE)
  hum_day <- hum_day %>%
    mutate(hum_bin = case_when(
      hum_day <= qs[1] ~ "저습(하위⅓)",
      hum_day <= qs[2] ~ "보통(중간⅓)",
      TRUE             ~ "고습(상위⅓)"
    ))

  d <- d0 %>% left_join(hum_day, by = "date")

  long <- d %>%
    pivot_longer(all_of(menu_cols), names_to = "menu", values_to = "orders") %>%
    group_by(date, period) %>%
    mutate(total_period = sum(orders, na.rm = TRUE)) %>%
    ungroup() %>%
    filter(total_period > 0) %>%
    mutate(share = orders / total_period) %>%
    select(date, period, hum_bin, menu, share)

  sum_share <- long %>%
    group_by(hum_bin, period, menu) %>%
    summarise(
      n_days     = n_distinct(date),
      mean_share = mean(share, na.rm = TRUE),
      se         = sd(share, na.rm = TRUE) / sqrt(n_days),
      .groups = "drop"
    ) %>%
    filter(n_days >= min_days) %>%
    mutate(
      period  = factor(period, levels = c("점심(11–14)","저녁(17–20)","야식(21–23)")),
      hum_bin = factor(hum_bin, levels = c("저습(하위⅓)","보통(중간⅓)","고습(상위⅓)"))
    )

  # 패싯 내부 정렬: 점유율 높은 메뉴가 위로
  sum_share <- sum_share %>%
    group_by(hum_bin, period) %>%
    mutate(menu = fct_reorder(menu, mean_share, .desc = TRUE)) %>%
    ungroup()

  # 동적 팔레트
  pal <- setNames(hue_pal()(length(levels(sum_share$menu))), levels(sum_share$menu))

  p <- ggplot(sum_share, aes(x = menu, y = mean_share, fill = menu)) +
    geom_col(width = 0.65, alpha = 0.95) +
    geom_errorbar(aes(ymin = pmax(0, mean_share - 1.96 * se),
                      ymax = mean_share + 1.96 * se),
                  width = 0.18, linewidth = 0.6) +
    coord_flip() +
    facet_grid(hum_bin ~ period, scales = "fixed") +
    scale_y_continuous(labels = percent_format(accuracy = 1)) +
    scale_fill_manual(values = pal, guide = "none") +
    labs(
      title = "습도 3분위 × 시간대별 메뉴 점유율(%) — 전체 메뉴",
      subtitle = paste0("각 칸: 날짜별 시간대 점유율 평균 ±95% CI · 표본 n≥", min_days,
                        " (습도: 하루 평균 기준 3분위)"),
      x = NULL, y = "점유율(%)"
    ) +
    theme(
      strip.text = element_text(face = "bold"),
      axis.text.y = element_text(size = 10),
      panel.grid.minor = element_blank(),
      plot.title.position = "plot"
    )

  return(p)
}

# 사용 예시
p_all <- plot_humidity_menu_share_all(df, min_days = 12)
print(p_all)

요약

습도 단계 점심 특징 저녁 특징 야식 특징 눈여겨볼 변화
저습 간단한 전통식·분식 위주 여전히 가벼움 피자·치킨 약간 전체적으로 안정적
보통 다양한 메뉴 혼재 치킨·돼지고기 급상 야식 쏠림 시작 선택 폭 최대
고습 따뜻한 식사류 증가 돼지고기·치킨 강세 치킨·야식 메뉴 폭발 기름기 높은 메뉴 집중
  • 습도가 오를수록 ‘치킨·돼지고기·피자’ 등 고지방 메뉴의 비중이 급격히 상승하고,

  • 특히 야식 시간대(21–23시)에 그 경향이 가장 극단적으로 강화됨.

  • 반면 건조할수록 한식/분식/패스트푸드 같은 가벼운 식사형 메뉴가 안정적으로 유지되는 패턴이야.

비 영향 분리를 통한 순수 습도 효과와 비의 효과

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales)
})

# ─────────────────────────────────────────────
# 🌦 신뢰도 중심 버전: 공통 분위, min_days=10 유지
# ─────────────────────────────────────────────
plot_humidity_rain_menu_share <- function(df, min_days = 10){

  menu_cols <- intersect(
    c("kr","snack","cafe","jp","fish","chicken","pizza",
      "asian_western","ch","pig","night","steam","boxed","fast"),
    names(df)
  )

  # 1) 기본 전처리
  d0 <- df %>%
    mutate(
      date = as.Date(date),
      hour = pmax(0, pmin(23, as.integer(time))),
      period = case_when(
        hour %in% 11:14 ~ "점심(11–14)",
        hour %in% 17:20 ~ "저녁(17–20)",
        hour %in% 21:23 ~ "야식(21–23)",
        TRUE ~ NA_character_
      ),
      rain_grp = if_else(rain > 0, "비 있음", "비 없음")
    ) %>%
    filter(!is.na(period))

  # 2) 하루 평균 습도 기반 3분위 (공통 기준)
  hum_day <- d0 %>%
    group_by(date) %>%
    summarise(hum_day = mean(hum, na.rm = TRUE), .groups = "drop")

  q <- quantile(hum_day$hum_day, probs = c(1/3, 2/3), na.rm = TRUE)
  hum_day <- hum_day %>%
    mutate(hum_bin = case_when(
      hum_day <= q[1] ~ "저습(하위⅓)",
      hum_day <= q[2] ~ "보통(중간⅓)",
      TRUE             ~ "고습(상위⅓)"
    ))

  d <- d0 %>% left_join(hum_day, by = "date")

  # 3) 메뉴별 점유율 계산
  long <- d %>%
    pivot_longer(all_of(menu_cols), names_to = "menu", values_to = "orders") %>%
    group_by(date, period, rain_grp) %>%
    mutate(total_period = sum(orders, na.rm = TRUE)) %>%
    ungroup() %>%
    filter(total_period > 0) %>%
    mutate(share = orders / total_period) %>%
    select(date, period, hum_bin, rain_grp, menu, share)

  # 4) 평균 및 95% CI 계산
  summary_tbl <- long %>%
    group_by(rain_grp, hum_bin, period, menu) %>%
    summarise(
      n_days = n_distinct(date),
      mean_share = mean(share, na.rm = TRUE),
      se = sd(share, na.rm = TRUE) / sqrt(n_days),
      ci_low = pmax(0, mean_share - 1.96 * se),
      ci_high = mean_share + 1.96 * se,
      .groups = "drop"
    ) %>%
    filter(n_days >= min_days) %>%
    mutate(
      hum_bin = factor(hum_bin, levels = c("저습(하위⅓)","보통(중간⅓)","고습(상위⅓)")),
      period  = factor(period,  levels = c("점심(11–14)","저녁(17–20)","야식(21–23)"))
    )

  # 5) 시각화 (비 있음/없음 비교)
  ggplot(summary_tbl, aes(menu, mean_share, fill = rain_grp)) +
    geom_col(position = position_dodge(width = 0.8), width = 0.7) +
    geom_errorbar(
      aes(ymin = ci_low, ymax = ci_high),
      position = position_dodge(width = 0.8),
      width = 0.15, linewidth = 0.5
    ) +
    facet_grid(hum_bin ~ period, scales = "free_y") +
    scale_y_continuous(labels = percent_format(accuracy = 1)) +
    labs(
      title = "습도 3분위 × 시간대별 메뉴 점유율(%) ±95% CI",
      subtitle = "비 있음 vs 비 없음 비교 (공통 습도 분위 기준, n≥10)",
      x = "메뉴", y = "점유율(%)", fill = "비 여부"
    ) +
    theme_minimal(base_size = 12) +
    theme(legend.position = "bottom")
}

# 실행
p <- plot_humidity_rain_menu_share(df, min_days = 10)
print(p)

요약

구간 주요 메뉴 트렌드 날씨 영향 해석
저습 × 모든 시간대 한식, 분식, 패스트푸드 중심 건조한 날은 기름진 음식 기피, 기본형 식사 유지
보통 × 저녁 비 있음일 때 치킨·피자 급등 비가 심리적 보상소비 유발 (“비 오는 날 치킨”)
고습 × 저녁/야식 비 있음 시 폭발적 증가 (치킨 5%↑, 돼지고기 3%↑) 습도+비 결합이 강한 배달 유인 요인
전 시간대 공통 비 없는 날은 안정적, 비 오는 날은 고칼로리 선호 강화 불쾌지수↑ → 즉흥적 ’기분전환 소비’로 전환
  • 습도만으로는 기름진 메뉴의 완만한 증가가 관찰되지만, 비가 내릴 때 상승폭이 수배로 커진다.

  • 비는 단순한 기상 조건이 아니라 ‘행동 트리거’**로 작용.

    → 외출 억제 → 배달 선호 → 기름진 메뉴 소비 급증.

  • 시간대별로 보면 저녁–야식 중심, 점심에는 상대적으로 안정적.

  • 치킨·돼지고기·피자가 ’비+습도 결합형 대표 메뉴’로 명확히 자리 잡음.

  • 한식·분식·카페류는 비 오는 날 오히려 점유율이 줄며, “정서적 편안함보다 즉흥적 만족감”을 택하는 경향으로 이동.

비 × 온도/습도 상호작용

suppressPackageStartupMessages({
  library(dplyr); library(tidyr); library(lubridate)
  library(ggplot2); library(scales); library(forcats)
})

# ─────────────────────────────────────────────
# 0) 공통 요약: 비 × 온도(3) × 습도(3) × 시간대(3)
# ─────────────────────────────────────────────
build_summary_4way <- function(df, min_days = 8){
  menu_cols <- intersect(
    c("kr","snack","cafe","jp","fish","chicken","pizza",
      "asian_western","ch","pig","night","steam","boxed","fast"),
    names(df)
  )
  stopifnot(length(menu_cols) > 0)

  # 표준 레벨(항상 이 순서로 보이게)
  period_lv  <- c("점심(11–14)","저녁(17–20)","야식(21–23)")
  tem_lv     <- c("저온(하위⅓)","보통(중간⅓)","고온(상위⅓)")
  hum_lv     <- c("건조(하위⅓)","보통(중간⅓)","습함(상위⅓)")

  d0 <- df %>%
    mutate(
      date   = as.Date(date),
      hour   = pmax(0, pmin(23, as.integer(time))),
      period = case_when(
        hour %in% 11:14 ~ "점심(11–14)",
        hour %in% 17:20 ~ "저녁(17–20)",
        hour %in% 21:23 ~ "야식(21–23)",
        TRUE ~ NA_character_
      ),
      rain_grp = if_else(rain > 0, "비 있음", "비 없음")
    ) %>%
    filter(!is.na(period))

  # 온도/습도 3분위(공통 기준)
  q_tem <- quantile(d0$tem, c(1/3, 2/3), na.rm = TRUE)
  q_hum <- quantile(d0$hum, c(1/3, 2/3), na.rm = TRUE)

  d0 <- d0 %>%
    mutate(
      tem_bin = case_when(
        tem <= q_tem[1] ~ "저온(하위⅓)",
        tem <= q_tem[2] ~ "보통(중간⅓)",
        TRUE             ~ "고온(상위⅓)"
      ),
      hum_bin = case_when(
        hum <= q_hum[1] ~ "건조(하위⅓)",
        hum <= q_hum[2] ~ "보통(중간⅓)",
        TRUE             ~ "습함(상위⅓)"
      ),
      period  = factor(period,  levels = period_lv),
      tem_bin = factor(tem_bin, levels = tem_lv),
      hum_bin = factor(hum_bin, levels = hum_lv)
    )

  # 메뉴 점유율(날짜×시간대×날씨 조합 대비 비중)
  long <- d0 %>%
    pivot_longer(all_of(menu_cols), names_to = "menu", values_to = "orders") %>%
    group_by(date, period, rain_grp, tem_bin, hum_bin) %>%
    mutate(total = sum(orders, na.rm = TRUE)) %>%
    ungroup() %>%
    filter(total > 0) %>%
    mutate(share = orders / total)

  # 평균 ±95% CI
  summary_tbl <- long %>%
    group_by(period, rain_grp, tem_bin, hum_bin, menu) %>%
    summarise(
      n_days = n_distinct(date),
      mean_share = mean(share, na.rm = TRUE),
      se = sd(share, na.rm = TRUE) / sqrt(n_days),
      ci_low = pmax(0, mean_share - 1.96*se),
      ci_high = mean_share + 1.96*se,
      .groups = "drop"
    ) %>%
    filter(n_days >= min_days) %>%
    mutate(
      period  = factor(period,  levels = period_lv),
      tem_bin = factor(tem_bin, levels = tem_lv),
      hum_bin = factor(hum_bin, levels = hum_lv)
    )

  return(summary_tbl)
}

# ─────────────────────────────────────────────
# 1) Δ그래프(비−무비): 시간대 ALL + 건조 패널 포함(drop=FALSE)
# ─────────────────────────────────────────────
plot_rain_delta_by_period <- function(summary_tbl,
                                      period_pick = c("ALL","점심(11–14)","저녁(17–20)","야식(21–23)"),
                                      top_n = 6,
                                      menu_focus = NULL,
                                      min_days_delta = 5,   # Δ계산 최소 표본(비/무비 각각)
                                      show_empty_panels = TRUE){

  period_pick <- match.arg(period_pick)

  # 표준 레벨 고정(건조 포함)
  period_lv  <- levels(summary_tbl$period)  %||% c("점심(11–14)","저녁(17–20)","야식(21–23)")
  tem_lv     <- levels(summary_tbl$tem_bin) %||% c("저온(하위⅓)","보통(중간⅓)","고온(상위⅓)")
  hum_lv     <- levels(summary_tbl$hum_bin) %||% c("건조(하위⅓)","보통(중간⅓)","습함(상위⅓)")

  dat0 <- if (period_pick == "ALL") summary_tbl else dplyr::filter(summary_tbl, period == period_pick)
  if (!is.null(menu_focus)) dat0 <- dplyr::filter(dat0, menu %in% menu_focus)

  # Δ계산용 표본 기준 적용
  dat0 <- dat0 %>% dplyr::filter(n_days >= min_days_delta)

  wide <- dat0 %>%
    mutate(rain_key = if_else(rain_grp == "비 있음", "yes", "no")) %>%
    select(period, tem_bin, hum_bin, menu, rain_key, mean_share, se) %>%
    pivot_wider(names_from = rain_key, values_from = c(mean_share, se), names_sep = "_")

  delta <- wide %>%
    filter(!is.na(mean_share_yes), !is.na(mean_share_no)) %>%
    transmute(
      period = factor(period, levels = period_lv),
      tem_bin = factor(tem_bin, levels = tem_lv),
      hum_bin = factor(hum_bin, levels = hum_lv),
      menu,
      delta    = mean_share_yes - mean_share_no,
      se_delta = sqrt(se_yes^2 + se_no^2),
      ci_low   = delta - 1.96 * se_delta,
      ci_high  = delta + 1.96 * se_delta
    )

  # 패널별 |Δ| Top-N만
  panel_keys <- c("tem_bin","hum_bin", if (period_pick == "ALL") "period" else NULL)

  keep <- delta %>%
    group_by(across(all_of(panel_keys))) %>%
    slice_max(order_by = abs(delta), n = top_n, with_ties = FALSE) %>%
    ungroup()

  dat <- delta %>%
    semi_join(keep, by = c(panel_keys, "menu")) %>%
    group_by(across(all_of(panel_keys))) %>%
    mutate(menu = fct_reorder(menu, delta, .desc = TRUE)) %>%
    ungroup()

  facet_formula <- if (period_pick == "ALL") hum_bin ~ tem_bin + period else hum_bin ~ tem_bin

  gg <- ggplot(dat, aes(x = menu, y = delta, fill = delta > 0)) +
    geom_col(width = 0.66) +
    geom_errorbar(aes(ymin = ci_low, ymax = ci_high), width = 0.15, linewidth = 0.5) +
    geom_hline(yintercept = 0, linewidth = 0.4, linetype = 2) +
    coord_flip() +
    facet_grid(facet_formula,
               scales = "free_y",
               drop   = !show_empty_panels) +  # 건조 패널까지 프레임 유지
    scale_y_continuous(labels = percent_format(accuracy = 0.1)) +
    scale_fill_manual(values = c(`TRUE` = "#1E88E5", `FALSE` = "#F4511E"),
                      labels = c(`TRUE` = "비 오면 증가", `FALSE` = "비 오면 감소"),
                      guide  = guide_legend(title = NULL)) +
    labs(
      title = paste0("비 효과(Δ = 비 있음 − 비 없음)",
                     if (period_pick == "ALL") " — 모든 시간대" else paste0(" — 시간대: ", period_pick)),
      subtitle = paste0("패널별 |Δ| Top-", top_n,
                        " 메뉴 (Δ 계산 최소 표본 n≥", min_days_delta, " / 빈 패널은 데이터 부족)"),
      x = NULL, y = "점유율 변화(%)"
    ) +
    theme_minimal(base_size = 12) +
    theme(
      legend.position = "bottom",
      panel.grid.minor = element_blank(),
      strip.text = element_text(face = "bold")
    )

  return(gg)
}

# ─────────────────────────────────────────────
# 2) 실행 예시 (건조 포함, 모든 시간대)
# ─────────────────────────────────────────────
sum4 <- build_summary_4way(df, min_days = 8)

# 모든 시간대 + 건조/보통/습함 전부 표시(drop=FALSE)
p_all <- plot_rain_delta_by_period(sum4,
                                   period_pick = "ALL",
                                   top_n = 6,
                                   min_days_delta = 5,
                                   show_empty_panels = TRUE)
print(p_all)

# 필요하면 저장
# ggsave("delta_all_with_dry.png", p_all, width = 14, height = 8.5, dpi = 220)

요약

구분 기상 조건 소비 행동 해석 대표 메뉴 변화
여름 장맛비 (고온 + 습함) 무더운 날씨 + 비 더위·습도·피로 누적 → 야식 및 기름진 메뉴 선호. 외출보다 집에서 배달 중심 치킨↑, 돼지고기↑, 패스트푸드↑
장마 저녁 (보통 온도 + 습함) 하루 피로 + 비 효과 퇴근 이후, 비 오는 날 배달 수요 집중. 기름진·익숙한 음식 선호 치킨↑, 분식↑, 한식↑
맑은 여름 (고온 + 비 없음) 덥지만 쾌적한 날씨 외식·야외활동 증가로 배달 수요 상대적 감소 steam(국물)·pig·kr 약보합(↓ 혹은 변화 없음)
  • ’장마철 비 오는 저녁’엔 치킨·분식·한식이 확실히 오르고,

  • ’습하고 더운 밤’엔 야식(치킨·돼지고기·패스트푸드)**가 뜨며,

  • ’맑고 더운 날’엔 배달보다 외식이 늘어난다.

인사이트 정리

  1. 주말·공휴일에는 전반적으로 주문량이 증가한다.

  2. 명절형 공휴일(설·추석)은 가족 이동과 휴무로 주문이 급감한다.

  3. 여가형 공휴일(어린이날·선거일)은 외출 후 배달 수요가 유지 또는 상승한다.

  4. 치킨은 연중 1위 카테고리로, 전체 시장의 기준점 역할을 한다.

  5. 겨울에는 돼지고기·찜류가 강세를 보인다.

  6. 여름에는 카페·패스트푸드 수요가 약세를 보인다.

  7. 점심엔 한식·분식, 저녁엔 치킨·고기, 야식엔 치킨·돼지고기가 중심이다.

  8. 주말은 저녁 피크가 늦고, 야식 수요가 길게 이어진다.

  9. 습도가 높을수록 하루 전체 주문량이 증가한다.

  10. 특히 저녁(18–19시)에 습도의 영향이 가장 크게 나타난다.

  11. 습한 날에는 치킨·고기·찜 등 묵직한 메뉴 비중이 높아진다.

  12. 건조한 날에는 카페·패스트푸드·일식 같은 가벼운 메뉴가 상대적으로 강세다.

  13. 약한 비(0–2mm)는 점심·저녁 주문을 늘리고, 폭우는 저녁을 줄이며 야식으로 이동시킨다.

  14. 비가 오면 전체 주문량뿐 아니라 메뉴 구성 자체가 바뀐다.

  15. 강수량이 많을수록 치킨 쏠림이 강해진다.

  16. 흐린 날과 비 오는 날은 평일 저녁에 주문이 폭발적으로 증가한다.

  17. 맑은 날은 외출로 인해 배달보다 외식이 늘어난다.

  18. 명절에는 피자·패스트푸드 같은 공유형 메뉴 비중이 상승한다.

  19. 여가형 공휴일에는 치킨·피자 중심의 간편 저녁 수요가 높다.

  20. 폭우·고습·금요일이 겹칠 때 주문량이 폭등한다.

  21. 주말은 점심 중심, 평일은 저녁 중심의 소비 패턴을 보인다.

  22. 고습한 날의 야식 시간대(21–23시)는 치킨·돼지고기·피자의 절정 구간이다.

  23. 비는 단순한 기상 조건이 아니라 행동 트리거(외출 억제 → 배달 촉진) 역할을 한다.

  24. ‘비 + 더위(고온+습함)’ 조합은 치킨·패스트푸드·고기류 소비를 폭발시킨다.

  25. 맑고 더운 날엔 배달보다 외식·카페 이용이 늘어난다.