การใช้ฟังก์ชันกับทุกแถวของตารางโดยใช้ dplyr?


121

เมื่อทำงานกับplyrฉันมักจะพบว่าการใช้adplyฟังก์ชันสเกลาร์มีประโยชน์ที่ฉันต้องใช้กับแต่ละแถว

เช่น

data(iris)
library(plyr)
head(
     adply(iris, 1, transform , Max.Len= max(Sepal.Length,Petal.Length))
    )
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species Max.Len
1          5.1         3.5          1.4         0.2  setosa     5.1
2          4.9         3.0          1.4         0.2  setosa     4.9
3          4.7         3.2          1.3         0.2  setosa     4.7
4          4.6         3.1          1.5         0.2  setosa     4.6
5          5.0         3.6          1.4         0.2  setosa     5.0
6          5.4         3.9          1.7         0.4  setosa     5.4

ตอนนี้ฉันใช้dplyrมากขึ้นฉันสงสัยว่ามีวิธีที่เป็นระเบียบ / เป็นธรรมชาติในการทำเช่นนี้หรือไม่? เนื่องจากนี่ไม่ใช่สิ่งที่ฉันต้องการ:

library(dplyr)
head(
     mutate(iris, Max.Len= max(Sepal.Length,Petal.Length))
    )
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species Max.Len
1          5.1         3.5          1.4         0.2  setosa     7.9
2          4.9         3.0          1.4         0.2  setosa     7.9
3          4.7         3.2          1.3         0.2  setosa     7.9
4          4.6         3.1          1.5         0.2  setosa     7.9
5          5.0         3.6          1.4         0.2  setosa     7.9
6          5.4         3.9          1.7         0.4  setosa     7.9

ฉันเพิ่งถามว่ามีเทียบเท่าmdplyใน dplyr doและฮัดลีย์บอกว่าพวกเขาอาจจะมีบางสิ่งบางอย่างขึ้นอยู่กับการผลิตเบียร์ ฉันเดาว่ามันก็น่าจะใช้ได้เช่นกัน
baptiste

4
ในที่สุด dplyr จะมีบางอย่างที่ต้องการrowwise()ซึ่งจะจัดกลุ่มตามแต่ละแถว
hadley

@hadley thx มันไม่ควรทำตัวเหมือนadplyตอนที่คุณไม่ใช้การจัดกลุ่มเหรอ? เนื่องจากฟังก์ชันรวมอย่างใกล้ชิดเรียกว่าgroup_byNOTsplit_by
Stephen Henderson

@StephenHenderson ไม่เพราะคุณต้องการวิธีการทำงานบนโต๊ะโดยรวม
hadley

1
@HowYaDoing ใช่ แต่วิธีนั้นไม่ได้สรุป ไม่มี psum, pmean หรือ pmedian เช่น
Stephen Henderson

คำตอบ:


202

เมื่อใช้ dplyr 0.2 (ฉันคิดว่า) rowwise()ดังนั้นคำตอบสำหรับปัญหานี้จึงกลายเป็น:

iris %>% 
  rowwise() %>% 
  mutate(Max.Len= max(Sepal.Length,Petal.Length))

ไม่ใช่rowwiseทางเลือกอื่น

ห้าปี (!) ต่อมาคำตอบนี้ยังคงได้รับการเข้าชมจำนวนมาก เนื่องจากได้รับแล้วrowwiseจึงไม่แนะนำมากขึ้นแม้ว่าผู้คนจำนวนมากดูเหมือนจะใช้งานง่าย ทำสิ่งที่ตัวเองชอบและทำตามขั้นตอนการทำงานแบบ Row-orientedของ Jenny Bryan ใน R ด้วยเนื้อหาที่เป็นระเบียบเรียบร้อยเพื่อให้จัดการกับหัวข้อนี้ได้ดี

วิธีที่ตรงไปตรงมาที่สุดที่ฉันพบมาจากตัวอย่างของ Hadley โดยใช้pmap:

iris %>% 
  mutate(Max.Len= purrr::pmap_dbl(list(Sepal.Length, Petal.Length), max))

เมื่อใช้วิธีนี้คุณสามารถระบุจำนวนอาร์กิวเมนต์ให้กับฟังก์ชัน ( .f) ภายในpmapได้

pmap เป็นแนวทางแนวคิดที่ดีเพราะมันสะท้อนให้เห็นถึงความจริงที่ว่าเมื่อคุณกำลังดำเนินการแถวอย่างชาญฉลาดคุณกำลังทำงานกับสิ่งทอจากรายการเวกเตอร์ (คอลัมน์ในดาต้าเฟรม)


ฉันได้เปลี่ยนสิ่งนี้ (จากด้านบน) เป็นคำตอบในอุดมคติเพราะฉันคิดว่านี่เป็นการใช้งานที่ตั้งใจไว้
Stephen Henderson

1
เป็นไปได้หรือไม่ที่จะเพิ่มค่าของ datatframe ที่สร้างแบบไดนามิก ดังนั้นในกรอบข้อมูลนี้จึงไม่ทราบชื่อคอลัมน์ ฉันสามารถเพิ่มได้หากรู้จักชื่อคอลัมน์
อรุณราชา

stackoverflow.com/questions/28807266/…เพิ่งพบคำตอบ ในนี้พวกเขาใช้สหสัมพันธ์แทนผลรวม แต่แนวคิดเดียวกัน.
อรุณราชา

13
หากไม่ได้ผลตรวจสอบให้แน่ใจว่าคุณใช้ dplyr :: mutate ไม่ใช่ plyr :: mutate - ทำให้ฉันบ้า
Jan-glx

ขอบคุณ YAK ฉันก็เช่นกัน หากคุณรวมทั้งสองอย่างplyrและdplyrแพ็กเกจคุณเกือบจะใช้ผิดประเภทmutateเว้นแต่คุณจะระบุขอบเขตdplyr::mutateไว้อย่างชัดเจน
Chris Warth

22

แนวทางสำนวนคือการสร้างฟังก์ชัน vectorised ที่เหมาะสม

Rระบุpmaxสิ่งที่เหมาะสมที่นี่อย่างไรก็ตามมันยังมีVectorizeเป็นกระดาษห่อหุ้มmapplyเพื่อให้คุณสามารถสร้างฟังก์ชันตามอำเภอใจในเวอร์ชันเวกเตอร์ได้

library(dplyr)
# use base R pmax (vectorized in C)
iris %>% mutate(max.len = pmax(Sepal.Length, Petal.Length))
# use vectorize to create your own function
# for example, a horribly inefficient get first non-Na value function
# a version that is not vectorized
coalesce <- function(a,b) {r <- c(a[1],b[1]); r[!is.na(r)][1]}
# a vectorized version
Coalesce <- Vectorize(coalesce, vectorize.args = c('a','b'))
# some example data
df <- data.frame(a = c(1:5,NA,7:10), b = c(1:3,NA,NA,6,NA,10:8))
df %>% mutate(ab =Coalesce(a,b))

โปรดทราบว่าการใช้ vectorization ใน C / C ++ จะเร็วกว่า แต่ไม่มีmagicPonyแพ็คเกจที่จะเขียนฟังก์ชันให้คุณ


ขอบคุณนี่เป็นคำตอบที่ยอดเยี่ยมเป็นสไตล์ R ทั่วไปที่ยอดเยี่ยมอย่างที่คุณพูด แต่ฉันไม่คิดว่ามันจะตอบคำถามของฉันได้จริงหรือไม่ว่าจะมีdplyrวิธี ... เพราะมันจะง่ายกว่านี้หากไม่มี dplyr เช่นwith(df, Coalesce(a,b))บางทีนั่นอาจเป็น คำตอบแบบนั้น - ไม่ใช้dplyrเพื่อ?
Stephen Henderson

4
ต้องยอมรับว่าฉันตรวจสอบอีกครั้งว่าไม่มีmagicPonyแพ็คเกจ แย่จัง
rsoren

21

คุณต้องจัดกลุ่มตามแถว:

iris %>% group_by(1:n()) %>% mutate(Max.Len= max(Sepal.Length,Petal.Length))

นี่คือสิ่งที่ทำใน1adply


ดูเหมือนว่าควรมีไวยากรณ์ที่ง่ายกว่านี้หรือ "ดีกว่า"
Stephen Henderson

@StephenHenderson อาจมีฉันไม่ใช่dplyrผู้เชี่ยวชาญ หวังว่าจะมีคนอื่นมาพร้อมกับสิ่งที่ดีกว่า หมายเหตุฉันทำความสะอาดเล็กน้อยด้วย1:n().
BrodieG

ฉันสงสัยว่าคุณพูดถูก แต่ฉันรู้สึกว่าพฤติกรรมเริ่มต้นที่ไม่มีการรวมกลุ่มควรเป็นเหมือนgroup_by(1:n())พฤติกรรม ถ้าไม่มีใครมีความคิดอื่นในตอนเช้าฉันจะทำเครื่องหมายของคุณ)
Stephen Henderson

นอกจากนี้โปรดทราบว่าสิ่งนี้ค่อนข้างขัดต่อเอกสารสำหรับn: "ฟังก์ชันนี้มีการใช้งานแบบพิเศษสำหรับแหล่งข้อมูลแต่ละแหล่งและสามารถใช้ได้จากการสรุปภายในเท่านั้น" แม้ว่าดูเหมือนจะใช้ได้ผล
BrodieG

คุณสามารถอ้างถึง Sepal.Length และ Petal.Length ด้วยหมายเลขดัชนีได้หรือไม่? หากคุณมีตัวแปรจำนวนมากก็จะเป็นประโยชน์ เช่น ... Max.len = max ([c (1,3)])?
Rasmus Larsen

19

Update 2017-08-03

หลังจากเขียนสิ่งนี้ Hadley ได้เปลี่ยนบางสิ่งอีกครั้ง ฟังก์ชั่นที่เคยอยู่ใน purrr ตอนนี้อยู่ในแพ็คเกจผสมใหม่ที่เรียกว่าpurrrlyrซึ่งอธิบายว่า:

purrrlyr มีฟังก์ชันบางอย่างที่อยู่ตรงจุดตัดของ purrr และ dplyr พวกเขาถูกลบออกจากเสียงฟี้ดเพื่อให้แพคเกจมีน้ำหนักเบาและเนื่องจากถูกแทนที่ด้วยโซลูชันอื่น ๆ ใน tidyverse

ดังนั้นคุณจะต้องติดตั้ง + โหลดแพ็คเกจนั้นเพื่อให้โค้ดด้านล่างใช้งานได้

โพสต์ต้นฉบับ

Hadley มักจะเปลี่ยนใจเกี่ยวกับสิ่งที่เราควรใช้ แต่ฉันคิดว่าเราควรจะเปลี่ยนไปใช้ฟังก์ชั่นในpurrrเพื่อรับฟังก์ชันทีละแถว อย่างน้อยพวกเขามีฟังก์ชันการทำงานที่เหมือนกันและมีเกือบอินเตอร์เฟซเดียวกันเป็นadplyจากplyr

มีสองฟังก์ชันที่เกี่ยวข้องby_rowและinvoke_rows. ความเข้าใจของฉันคือคุณใช้by_rowเมื่อคุณต้องการวนซ้ำแถวและเพิ่มผลลัพธ์ลงใน data.frame invoke_rowsใช้เมื่อคุณวนซ้ำแถวของ data.frame และส่งแต่ละ col เป็นอาร์กิวเมนต์ไปยังฟังก์ชัน เราจะใช้ครั้งแรกเท่านั้น

ตัวอย่าง

library(tidyverse)

iris %>% 
  by_row(..f = function(this_row) {
    browser()
  })

นี้จะช่วยให้เราเห็น internals (เพื่อเราจะได้เห็นสิ่งที่เรากำลังทำอยู่) adplyซึ่งเป็นเช่นเดียวกับการทำมันด้วย

Called from: ..f(.d[[i]], ...)
Browse[1]> this_row
# A tibble: 1 × 5
  Sepal.Length Sepal.Width Petal.Length Petal.Width Species
         <dbl>       <dbl>        <dbl>       <dbl>  <fctr>
1          5.1         3.5          1.4         0.2  setosa
Browse[1]> Q

ตามค่าเริ่มต้นby_rowจะเพิ่มคอลัมน์รายการตามผลลัพธ์:

iris %>% 
  by_row(..f = function(this_row) {
      this_row[1:4] %>% unlist %>% mean
  })

ให้:

# A tibble: 150 × 6
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species      .out
          <dbl>       <dbl>        <dbl>       <dbl>  <fctr>    <list>
1           5.1         3.5          1.4         0.2  setosa <dbl [1]>
2           4.9         3.0          1.4         0.2  setosa <dbl [1]>
3           4.7         3.2          1.3         0.2  setosa <dbl [1]>
4           4.6         3.1          1.5         0.2  setosa <dbl [1]>
5           5.0         3.6          1.4         0.2  setosa <dbl [1]>
6           5.4         3.9          1.7         0.4  setosa <dbl [1]>
7           4.6         3.4          1.4         0.3  setosa <dbl [1]>
8           5.0         3.4          1.5         0.2  setosa <dbl [1]>
9           4.4         2.9          1.4         0.2  setosa <dbl [1]>
10          4.9         3.1          1.5         0.1  setosa <dbl [1]>
# ... with 140 more rows

ถ้าเราส่งคืน a แทนเราdata.frameจะได้รับรายการด้วยdata.frames:

iris %>% 
  by_row( ..f = function(this_row) {
    data.frame(
      new_col_mean = this_row[1:4] %>% unlist %>% mean,
      new_col_median = this_row[1:4] %>% unlist %>% median
    )
  })

ให้:

# A tibble: 150 × 6
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species                 .out
          <dbl>       <dbl>        <dbl>       <dbl>  <fctr>               <list>
1           5.1         3.5          1.4         0.2  setosa <data.frame [1 × 2]>
2           4.9         3.0          1.4         0.2  setosa <data.frame [1 × 2]>
3           4.7         3.2          1.3         0.2  setosa <data.frame [1 × 2]>
4           4.6         3.1          1.5         0.2  setosa <data.frame [1 × 2]>
5           5.0         3.6          1.4         0.2  setosa <data.frame [1 × 2]>
6           5.4         3.9          1.7         0.4  setosa <data.frame [1 × 2]>
7           4.6         3.4          1.4         0.3  setosa <data.frame [1 × 2]>
8           5.0         3.4          1.5         0.2  setosa <data.frame [1 × 2]>
9           4.4         2.9          1.4         0.2  setosa <data.frame [1 × 2]>
10          4.9         3.1          1.5         0.1  setosa <data.frame [1 × 2]>
# ... with 140 more rows

วิธีที่เราเพิ่มเอาต์พุตของฟังก์ชันนั้นถูกควบคุมโดย.collateพารามิเตอร์ มีสามตัวเลือก: รายการแถวคอลัมน์ เมื่อเอาต์พุตของเรามีความยาว 1 ไม่สำคัญว่าเราจะใช้แถวหรือคอลัมน์

iris %>% 
  by_row(.collate = "cols", ..f = function(this_row) {
    this_row[1:4] %>% unlist %>% mean
  })

iris %>% 
  by_row(.collate = "rows", ..f = function(this_row) {
    this_row[1:4] %>% unlist %>% mean
  })

ทั้งสองผลิต:

# A tibble: 150 × 6
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species  .out
          <dbl>       <dbl>        <dbl>       <dbl>  <fctr> <dbl>
1           5.1         3.5          1.4         0.2  setosa 2.550
2           4.9         3.0          1.4         0.2  setosa 2.375
3           4.7         3.2          1.3         0.2  setosa 2.350
4           4.6         3.1          1.5         0.2  setosa 2.350
5           5.0         3.6          1.4         0.2  setosa 2.550
6           5.4         3.9          1.7         0.4  setosa 2.850
7           4.6         3.4          1.4         0.3  setosa 2.425
8           5.0         3.4          1.5         0.2  setosa 2.525
9           4.4         2.9          1.4         0.2  setosa 2.225
10          4.9         3.1          1.5         0.1  setosa 2.400
# ... with 140 more rows

หากเราส่งออก data.frame ด้วย 1 แถวจะมีความสำคัญเพียงเล็กน้อยที่เราใช้

iris %>% 
  by_row(.collate = "cols", ..f = function(this_row) {
    data.frame(
      new_col_mean = this_row[1:4] %>% unlist %>% mean,
      new_col_median = this_row[1:4] %>% unlist %>% median
      )
  })

iris %>% 
  by_row(.collate = "rows", ..f = function(this_row) {
    data.frame(
      new_col_mean = this_row[1:4] %>% unlist %>% mean,
      new_col_median = this_row[1:4] %>% unlist %>% median
    )
  })

ทั้งสองให้:

# A tibble: 150 × 8
   Sepal.Length Sepal.Width Petal.Length Petal.Width Species  .row new_col_mean new_col_median
          <dbl>       <dbl>        <dbl>       <dbl>  <fctr> <int>        <dbl>          <dbl>
1           5.1         3.5          1.4         0.2  setosa     1        2.550           2.45
2           4.9         3.0          1.4         0.2  setosa     2        2.375           2.20
3           4.7         3.2          1.3         0.2  setosa     3        2.350           2.25
4           4.6         3.1          1.5         0.2  setosa     4        2.350           2.30
5           5.0         3.6          1.4         0.2  setosa     5        2.550           2.50
6           5.4         3.9          1.7         0.4  setosa     6        2.850           2.80
7           4.6         3.4          1.4         0.3  setosa     7        2.425           2.40
8           5.0         3.4          1.5         0.2  setosa     8        2.525           2.45
9           4.4         2.9          1.4         0.2  setosa     9        2.225           2.15
10          4.9         3.1          1.5         0.1  setosa    10        2.400           2.30
# ... with 140 more rows

ยกเว้นว่าคอลัมน์ที่สองมีการเรียกคอลัมน์.rowและคอลัมน์แรกไม่มี

สุดท้ายหากผลลัพธ์ของเรายาวกว่าความยาว 1 ไม่ว่าจะเป็น a vectorหรือเป็นdata.frameแถวก็มีความสำคัญไม่ว่าเราจะใช้แถวหรือคอลัมน์สำหรับ.collate:

mtcars[1:2] %>% by_row(function(x) 1:5)
mtcars[1:2] %>% by_row(function(x) 1:5, .collate = "rows")
mtcars[1:2] %>% by_row(function(x) 1:5, .collate = "cols")

ผลิตตามลำดับ:

# A tibble: 32 × 3
     mpg   cyl      .out
   <dbl> <dbl>    <list>
1   21.0     6 <int [5]>
2   21.0     6 <int [5]>
3   22.8     4 <int [5]>
4   21.4     6 <int [5]>
5   18.7     8 <int [5]>
6   18.1     6 <int [5]>
7   14.3     8 <int [5]>
8   24.4     4 <int [5]>
9   22.8     4 <int [5]>
10  19.2     6 <int [5]>
# ... with 22 more rows

# A tibble: 160 × 4
     mpg   cyl  .row  .out
   <dbl> <dbl> <int> <int>
1     21     6     1     1
2     21     6     1     2
3     21     6     1     3
4     21     6     1     4
5     21     6     1     5
6     21     6     2     1
7     21     6     2     2
8     21     6     2     3
9     21     6     2     4
10    21     6     2     5
# ... with 150 more rows

# A tibble: 32 × 7
     mpg   cyl .out1 .out2 .out3 .out4 .out5
   <dbl> <dbl> <int> <int> <int> <int> <int>
1   21.0     6     1     2     3     4     5
2   21.0     6     1     2     3     4     5
3   22.8     4     1     2     3     4     5
4   21.4     6     1     2     3     4     5
5   18.7     8     1     2     3     4     5
6   18.1     6     1     2     3     4     5
7   14.3     8     1     2     3     4     5
8   24.4     4     1     2     3     4     5
9   22.8     4     1     2     3     4     5
10  19.2     6     1     2     3     4     5
# ... with 22 more rows

ดังนั้นบรรทัดล่าง หากคุณต้องการadply(.margins = 1, ...)ฟังก์ชั่นคุณสามารถใช้by_row.


2
by_rowเลิกใช้แล้วโดยเรียกว่า "ใช้ชุดค่าผสมของ: tidyr :: nest (); dplyr :: mutate (); purrr :: map ()" github.com/hadley/purrrlyr/blob/…
momeara

นั่นคือ r มาก ๆ
qwr

14

ขยายคำตอบของ BrodieG

หากกลับมาทำงานมากกว่าหนึ่งแถวนั้นแทนmutate(), do()จะต้องใช้ จากนั้นให้รวมกลับเข้าด้วยกันใช้rbind_all()จากdplyrแพ็คเกจ

ในdplyrเวอร์ชันการdplyr_0.1.2ใช้1:n()ในgroup_by()ประโยคไม่ได้ผลสำหรับฉัน หวังว่าHadley จะใช้งานได้rowwise()เร็ว ๆ นี้

iris %>%
    group_by(1:nrow(iris)) %>%
    do(do_fn) %>%
    rbind_all()

การทดสอบประสิทธิภาพ

library(plyr)    # plyr_1.8.4.9000
library(dplyr)   # dplyr_0.8.0.9000
library(purrr)   # purrr_0.2.99.9000
library(microbenchmark)

d1_count <- 1000
d2_count <- 10

d1 <- data.frame(a=runif(d1_count))

do_fn <- function(row){data.frame(a=row$a, b=runif(d2_count))}
do_fn2 <- function(a){data.frame(a=a, b=runif(d2_count))}

op <- microbenchmark(
        plyr_version = plyr::adply(d1, 1, do_fn),
        dplyr_version = d1 %>%
            dplyr::group_by(1:nrow(d1)) %>%
            dplyr::do(do_fn(.)) %>%
            dplyr::bind_rows(),
        purrr_version = d1 %>% purrr::pmap_dfr(do_fn2),
        times=50)

มีผลลัพธ์ดังต่อไปนี้:

Unit: milliseconds
          expr       min        lq      mean    median        uq       max neval
  plyr_version 1227.2589 1275.1363 1317.3431 1293.5759 1314.4266 1616.5449    50
 dplyr_version  977.3025 1012.6340 1035.9436 1025.6267 1040.5882 1449.0978    50
 purrr_version  609.5790  629.7565  643.8498  644.2505  656.1959  686.8128    50

นี่แสดงว่าpurrrเวอร์ชันใหม่เร็วที่สุด


1

อะไรทำนองนี้?

iris$Max.Len <- pmax(iris$Sepal.Length, iris$Petal.Length)

1
ใช่ขอบคุณนั่นเป็นคำตอบที่เฉพาะเจาะจงมาก แต่ตัวอย่างและคำถามของฉันพยายามจะล้อว่ามีdplyrวิธีแก้ปัญหาทั่วไปสำหรับฟังก์ชันสเกลาร์หรือไม่
Stephen Henderson

โดยทั่วไปฟังก์ชันควรเป็นเวกเตอร์ - ถ้าเป็นฟังก์ชันแปลกประหลาดคุณอาจเขียนwacky.function <- function(col.1, col.2){...}แล้วiris.wacky <- wacky.function(iris$Sepal.Length, iris$Petal.Length)ก็ได้
colcarroll

บ่อยครั้งที่ฉันควรเดา แต่ฉันคิดว่าเมื่อคุณใช้สิ่งที่เหมือนdplyrหรือplyrหรือพูดว่าdata.tableคุณควรพยายามใช้สำนวนของพวกเขาเพื่อไม่ให้โค้ดของคุณกลายเป็นเรื่องยากที่จะแบ่งปันสไตล์ที่ผสมผสานกัน ดังนั้นคำถาม
Stephen Henderson

บรรทัดแรกของplyrเอกสารคือ "plyr เป็นชุดเครื่องมือที่ใช้แก้ปัญหาทั่วไป: คุณต้องแบ่งปัญหาใหญ่ออกเป็นชิ้นส่วนที่จัดการได้ดำเนินการในแต่ละชิ้นแล้วนำชิ้นส่วนทั้งหมดกลับมารวมกัน" ดูเหมือนว่าจะเป็นปัญหาที่แตกต่างกันมากซึ่งการดำเนินการคอลัมน์พื้นฐานเป็นเครื่องมือที่ดีที่สุด นอกจากนี้ยังอาจอธิบายได้ว่าเหตุใดจึงไม่มี "natural" plyr/ dplyrcommand สำหรับการทำเช่นนี้
colcarroll

5
ในการขายเนื้อคำพูดที่มีชื่อเสียง: " ถ้าทั้งหมดที่คุณมีคือ plyr คุณจะต้องใช้ค้อนและไขควงด้วย "
thelatemail
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.