วิธีใดเร็วที่สุดในการรวม / เข้าร่วม data.frames ใน R?


97

ตัวอย่างเช่น (ไม่แน่ใจว่าตัวอย่างที่เป็นตัวแทนส่วนใหญ่):

N <- 1e6
d1 <- data.frame(x=sample(N,N), y1=rnorm(N))
d2 <- data.frame(x=sample(N,N), y2=rnorm(N))

นี่คือสิ่งที่ฉันมีจนถึงตอนนี้:

d <- merge(d1,d2)
# 7.6 sec

library(plyr)
d <- join(d1,d2)
# 2.9 sec

library(data.table)
dt1 <- data.table(d1, key="x")
dt2 <- data.table(d2, key="x")
d <- data.frame( dt1[dt2,list(x,y1,y2=dt2$y2)] )
# 4.9 sec

library(sqldf)
sqldf()
sqldf("create index ix1 on d1(x)")
sqldf("create index ix2 on d2(x)")
d <- sqldf("select * from d1 inner join d2 on d1.x=d2.x")
sqldf()
# 17.4 sec

วิธีที่เหมาะสมในการทำวิธี sqldf ระบุไว้ด้านล่างโดย Gabor: สร้างเพียงดัชนีเดียว (พูดใน d1) และใช้ d1.main แทน d1 ในคำสั่ง select (มิฉะนั้นจะไม่ใช้ดัชนี) เวลาอยู่ในกรณีนี้คือ 13.6 วินาที การสร้างดัชนีบนตารางทั้งสองไม่จำเป็นในกรณี data.table เช่นกันเพียงแค่ทำ "dt2 <- data.table (d2)" และเวลาจะเป็น 3.9 วินาที
datasmurf

คำตอบทั้งสองให้ข้อมูลที่มีค่าควรค่าแก่การอ่านทั้งคู่ (แม้ว่าจะ "ยอมรับ" ได้เพียงคำเดียว
datasmurf

คุณกำลังเปรียบเทียบการเข้าร่วมด้านซ้ายกับการเข้าร่วมภายในในคำถามของคุณ
jangorecki

คำตอบ:


46

วิธีการจับคู่จะทำงานเมื่อมีคีย์ที่ไม่ซ้ำกันในเฟรมข้อมูลที่สองสำหรับแต่ละค่าคีย์ในครั้งแรก หากมีข้อมูลซ้ำกันในเฟรมข้อมูลที่สองวิธีการจับคู่และผสานจะไม่เหมือนกัน แน่นอนว่าการแข่งขันจะเร็วขึ้นเนื่องจากไม่ได้ทำมากนัก โดยเฉพาะอย่างยิ่งจะไม่มองหาคีย์ที่ซ้ำกัน (ต่อหลังรหัส)

DF1 = data.frame(a = c(1, 1, 2, 2), b = 1:4)
DF2 = data.frame(b = c(1, 2, 3, 3, 4), c = letters[1:5])
merge(DF1, DF2)
    b a c
  1 1 1 a
  2 2 1 b
  3 3 2 c
  4 3 2 d
  5 4 2 e
DF1$c = DF2$c[match(DF1$b, DF2$b)]
DF1$c
[1] a b c e
Levels: a b c d e

> DF1
  a b c
1 1 1 a
2 1 2 b
3 2 3 c
4 2 4 e

ในรหัส sqldf ที่โพสต์ในคำถามอาจดูเหมือนว่ามีการใช้ดัชนีในสองตาราง แต่ในความเป็นจริงแล้วดัชนีเหล่านี้ถูกวางไว้บนตารางที่ถูกเขียนทับก่อนที่ sql select จะทำงานและในบางส่วนก็อธิบายถึงสาเหตุ มันช้ามาก แนวคิดของ sqldf คือเฟรมข้อมูลในเซสชัน R ของคุณประกอบเป็นฐานข้อมูลไม่ใช่ตารางใน sqlite ดังนั้นทุกครั้งที่โค้ดอ้างถึงชื่อตารางที่ไม่มีเงื่อนไขจะดูในพื้นที่ทำงาน R ของคุณไม่ใช่ในฐานข้อมูลหลักของ sqlite ดังนั้นคำสั่ง select ที่แสดงจะอ่าน d1 และ d2 จากเวิร์กสเปซไปยังฐานข้อมูลหลักของ sqlite เพื่อคัดลอกสิ่งที่อยู่ที่นั่นด้วยดัชนี ด้วยเหตุนี้จึงเข้าร่วมโดยไม่มีดัชนี หากคุณต้องการใช้ประโยชน์จากเวอร์ชันของ d1 และ d2 ที่อยู่ในฐานข้อมูลหลักของ sqlite คุณจะต้องอ้างถึงเป็น main.d1 และ main d2 และไม่เป็น d1 และ d2 นอกจากนี้หากคุณพยายามทำให้มันทำงานได้เร็วที่สุดโปรดทราบว่าการเข้าร่วมแบบธรรมดาไม่สามารถใช้ประโยชน์จากดัชนีบนทั้งสองตารางได้ดังนั้นคุณจึงประหยัดเวลาในการสร้างดัชนีอย่างใดอย่างหนึ่ง ในโค้ดด้านล่างเราจะแสดงประเด็นเหล่านี้

มันคุ้มค่าที่จะสังเกตว่าการคำนวณที่แม่นยำสามารถสร้างความแตกต่างอย่างมากว่าแพ็คเกจใดเร็วที่สุด ตัวอย่างเช่นเราทำการรวมและการรวมด้านล่าง เราเห็นว่าผลลัพธ์เกือบจะกลับกันสำหรับทั้งสอง ในตัวอย่างแรกจากเร็วที่สุดไปยังช้าที่สุดเราได้รับ data.table, plyr, merge และ sqldf ในขณะที่ในตัวอย่างที่สอง sqldf, aggregate, data.table และ plyr - เกือบจะตรงกันข้ามกับอันแรก ในตัวอย่างแรก sqldf ช้ากว่า data.table 3 เท่าและในวินาทีนั้นเร็วกว่า plyr 200 เท่าและเร็วกว่า data.table 100 เท่า ด้านล่างนี้เราจะแสดงรหัสอินพุตการกำหนดเวลาเอาต์พุตสำหรับการผสานและการกำหนดเวลาเอาต์พุตสำหรับการรวม นอกจากนี้ยังควรสังเกตด้วยว่า sqldf ขึ้นอยู่กับฐานข้อมูลดังนั้นจึงสามารถจัดการกับวัตถุที่มีขนาดใหญ่กว่าที่ R สามารถจัดการได้ (ถ้าคุณใช้อาร์กิวเมนต์ dbname ของ sqldf) ในขณะที่วิธีการอื่น ๆ จำกัด เฉพาะการประมวลผลในหน่วยความจำหลัก นอกจากนี้เรายังได้แสดง sqldf ด้วย sqlite แต่ยังรองรับฐานข้อมูล H2 และ PostgreSQL ด้วย

library(plyr)
library(data.table)
library(sqldf)

set.seed(123)
N <- 1e5
d1 <- data.frame(x=sample(N,N), y1=rnorm(N))
d2 <- data.frame(x=sample(N,N), y2=rnorm(N))

g1 <- sample(1:1000, N, replace = TRUE)
g2<- sample(1:1000, N, replace = TRUE)
d <- data.frame(d1, g1, g2)

library(rbenchmark)

benchmark(replications = 1, order = "elapsed",
   merge = merge(d1, d2),
   plyr = join(d1, d2),
   data.table = { 
      dt1 <- data.table(d1, key = "x")
      dt2 <- data.table(d2, key = "x")
      data.frame( dt1[dt2,list(x,y1,y2=dt2$y2)] )
      },
   sqldf = sqldf(c("create index ix1 on d1(x)",
      "select * from main.d1 join d2 using(x)"))
)

set.seed(123)
N <- 1e5
g1 <- sample(1:1000, N, replace = TRUE)
g2<- sample(1:1000, N, replace = TRUE)
d <- data.frame(x=sample(N,N), y=rnorm(N), g1, g2)

benchmark(replications = 1, order = "elapsed",
   aggregate = aggregate(d[c("x", "y")], d[c("g1", "g2")], mean), 
   data.table = {
      dt <- data.table(d, key = "g1,g2")
      dt[, colMeans(cbind(x, y)), by = "g1,g2"]
   },
   plyr = ddply(d, .(g1, g2), summarise, avx = mean(x), avy=mean(y)),
   sqldf = sqldf(c("create index ix on d(g1, g2)",
      "select g1, g2, avg(x), avg(y) from main.d group by g1, g2"))
)

ผลลัพธ์จากการเรียกใช้เกณฑ์มาตรฐานสองรายการเปรียบเทียบการคำนวณการผสานคือ:

Joining by: x
        test replications elapsed relative user.self sys.self user.child sys.child
3 data.table            1    0.34 1.000000      0.31     0.01         NA        NA
2       plyr            1    0.44 1.294118      0.39     0.02         NA        NA
1      merge            1    1.17 3.441176      1.10     0.04         NA        NA
4      sqldf            1    3.34 9.823529      3.24     0.04         NA        NA

ผลลัพธ์จากการเรียกใช้เกณฑ์มาตรฐานเปรียบเทียบการคำนวณรวมคือ:

        test replications elapsed  relative user.self sys.self user.child sys.child
4      sqldf            1    2.81  1.000000      2.73     0.02         NA        NA
1  aggregate            1   14.89  5.298932     14.89     0.00         NA        NA
2 data.table            1  132.46 47.138790    131.70     0.08         NA        NA
3       plyr            1  212.69 75.690391    211.57     0.56         NA        NA

ขอบคุณ Gabor คะแนนดีเยี่ยมฉันได้ทำการปรับเปลี่ยนบางอย่างผ่านความคิดเห็นของคำถามเดิม อันที่จริงฉันเดาว่าลำดับอาจเปลี่ยนแปลงได้แม้ในกรณี "ผสาน" ขึ้นอยู่กับขนาดสัมพัทธ์ของตารางความหลากหลายของคีย์ ฯลฯ (นั่นคือเหตุผลที่ฉันบอกว่าฉันไม่แน่ใจว่าตัวอย่างของฉันเป็นตัวแทนหรือไม่) อย่างไรก็ตามเป็นเรื่องดีที่ได้เห็นวิธีแก้ไขปัญหาที่แตกต่างกันทั้งหมด
datasmurf

ฉันขอขอบคุณสำหรับความคิดเห็นเกี่ยวกับกรณี "การรวม" แม้ว่าจะแตกต่างจากการตั้งค่า "รวม" ในคำถาม แต่ก็มีความเกี่ยวข้องมาก ฉันจะได้ถามจริงเกี่ยวกับเรื่องนี้ในคำถามที่แยกกัน แต่มีอยู่แล้วที่นี่stackoverflow.com/questions/3685492/... คุณอาจต้องการมีส่วนร่วมกับสิ่งนั้นเช่นกันจากผลลัพธ์ข้างต้นโซลูชัน sqldf อาจเอาชนะคำตอบที่มีอยู่ทั้งหมดที่นั่น;)
datasmurf

40

132 วินาทีที่รายงานในผลลัพธ์ของ Gabor data.tableเป็นฟังก์ชันฐานเวลาcolMeansและcbind(การจัดสรรหน่วยความจำและการคัดลอกที่เกิดจากการใช้ฟังก์ชันเหล่านั้น) มีวิธีการใช้ที่ดีและไม่ดีdata.tableเช่นกัน

benchmark(replications = 1, order = "elapsed", 
  aggregate = aggregate(d[c("x", "y")], d[c("g1", "g2")], mean),
  data.tableBad = {
     dt <- data.table(d, key = "g1,g2") 
     dt[, colMeans(cbind(x, y)), by = "g1,g2"]
  }, 
  data.tableGood = {
     dt <- data.table(d, key = "g1,g2") 
     dt[, list(mean(x),mean(y)), by = "g1,g2"]
  }, 
  plyr = ddply(d, .(g1, g2), summarise, avx = mean(x), avy=mean(y)),
  sqldf = sqldf(c("create index ix on d(g1, g2)",
      "select g1, g2, avg(x), avg(y) from main.d group by g1, g2"))
  ) 

            test replications elapsed relative user.self sys.self
3 data.tableGood            1    0.15    1.000      0.16     0.00
5          sqldf            1    1.01    6.733      1.01     0.00
2  data.tableBad            1    1.63   10.867      1.61     0.01
1      aggregate            1    6.40   42.667      6.38     0.00
4           plyr            1  317.97 2119.800    265.12    51.05

packageVersion("data.table")
# [1] ‘1.8.2’
packageVersion("plyr")
# [1] ‘1.7.1’
packageVersion("sqldf")
# [1] ‘0.4.6.4’
R.version.string
# R version 2.15.1 (2012-06-22)

โปรดทราบว่าฉันไม่รู้จัก plyr ดีดังนั้นโปรดตรวจสอบกับ Hadley ก่อนที่จะอาศัยการplyrกำหนดเวลาที่นี่ นอกจากนี้โปรดทราบว่าdata.tabledo รวมถึงเวลาในการแปลงdata.tableและตั้งค่าคีย์สำหรับค่าโดยสาร


คำตอบนี้ได้รับการอัปเดตตั้งแต่แรกที่ตอบในเดือนธันวาคม 2010 ผลการเปรียบเทียบก่อนหน้าอยู่ด้านล่าง โปรดดูประวัติการแก้ไขของคำตอบนี้เพื่อดูว่ามีอะไรเปลี่ยนแปลงบ้าง

              test replications elapsed   relative user.self sys.self
4   data.tableBest            1   0.532   1.000000     0.488    0.020
7            sqldf            1   2.059   3.870301     2.041    0.008
3 data.tableBetter            1   9.580  18.007519     9.213    0.220
1        aggregate            1  14.864  27.939850    13.937    0.316
2  data.tableWorst            1 152.046 285.800752   150.173    0.556
6 plyrwithInternal            1 198.283 372.712406   189.391    7.665
5             plyr            1 225.726 424.296992   208.013    8.004

เนื่องจาก ddply ใช้งานได้เฉพาะกับ data frames เท่านั้นจึงเป็นตัวอย่างที่ให้ประสิทธิภาพในกรณีที่แย่ที่สุด ฉันหวังว่าจะมีอินเทอร์เฟซที่ดีขึ้นสำหรับการทำงานทั่วไปประเภทนี้ในเวอร์ชันอนาคต
hadley

1
FYI: คุณไม่สามารถใช้.Internalโทรในแพคเกจ CRAN ดูที่CRAN นโยบาย
Joshua Ulrich

@JoshuaUlrich คุณทำได้เมื่อคำตอบถูกเขียนเมื่อเกือบ 2 ปีที่แล้ว iirc ฉันจะอัปเดตคำตอบนี้data.tableโดยอัตโนมัติทันทีmean(โดยไม่ต้องโทร.Internalภายใน)
Matt Dowle

@MatthewDowle: ใช่ฉันไม่แน่ใจว่าเมื่อไหร่ / ถ้ามันเปลี่ยนไป ฉันเพิ่งรู้ว่ามันเป็นกรณีนี้ และมันก็ดีอย่างสมบูรณ์ในคำตอบของคุณไม่สามารถใช้งานได้ในแพ็คเกจ
Joshua Ulrich

1
@AleksandrBlekh ขอบคุณค่ะ ฉันได้เชื่อมโยงกับความคิดเห็นของคุณที่นี่เพื่อคุณลักษณะที่มีอยู่คำขอ# 599 ไปที่นั่นกันเถอะ โค้ดตัวอย่างของคุณแสดงให้เห็นถึงการforวนซ้ำเป็นสิ่งที่ดี คุณสามารถเพิ่มข้อมูลเพิ่มเติมเกี่ยวกับ "การวิเคราะห์ SEM" ในปัญหานั้นได้หรือไม่ ตัวอย่างเช่นฉันคาดเดาว่า SEM = Scanning electron microscope? การเรียนรู้เพิ่มเติมเกี่ยวกับแอปพลิเคชันทำให้น่าสนใจยิ่งขึ้นสำหรับเราและช่วยให้เราจัดลำดับความสำคัญ
Matt Dowle

16

สำหรับงานง่ายๆ (ค่าที่ไม่ซ้ำกันทั้งสองด้านของการเข้าร่วม) ฉันใช้match:

system.time({
    d <- d1
    d$y2 <- d2$y2[match(d1$x,d2$x)]
})

เร็วกว่าการผสาน (บนเครื่องของฉัน 0.13 ถึง 3.37 วินาที)

การกำหนดเวลาของฉัน:

  • merge: 3.32 วินาที
  • plyr: 0.84 วินาที
  • match: 0.12 วินาที

4
ขอบคุณ Marek คำอธิบายบางส่วนของเหตุผลที่เป็นเช่นนี้ได้อย่างรวดเร็ว (สร้างดัชนี / ตารางแฮช) สามารถพบได้ที่นี่: tolstoy.newcastle.edu.au/R/help/01c/2739.html
datasmurf

11

คิดว่ามันน่าสนใจที่จะโพสต์เกณฑ์มาตรฐานด้วย dplyr ในการผสมผสาน: (มีหลายสิ่งที่กำลังทำงานอยู่)

            test replications elapsed relative user.self sys.self
5          dplyr            1    0.25     1.00      0.25     0.00
3 data.tableGood            1    0.28     1.12      0.27     0.00
6          sqldf            1    0.58     2.32      0.57     0.00
2  data.tableBad            1    1.10     4.40      1.09     0.01
1      aggregate            1    4.79    19.16      4.73     0.02
4           plyr            1  186.70   746.80    152.11    30.27

packageVersion("data.table")
[1] ‘1.8.10’
packageVersion("plyr")
[1] ‘1.8’
packageVersion("sqldf")
[1] ‘0.4.7’
packageVersion("dplyr")
[1] ‘0.1.2’
R.version.string
[1] "R version 3.0.2 (2013-09-25)"

เพิ่งเพิ่ม:

dplyr = summarise(dt_dt, avx = mean(x), avy = mean(y))

และตั้งค่าข้อมูลสำหรับ dplyr ด้วยตารางข้อมูล:

dt <- tbl_dt(d)
dt_dt <- group_by(dt, g1, g2)

อัปเดต:ฉันลบ data.tableBad และ plyr และไม่มีอะไรนอกจากเปิด RStudio (i7, 16GB ram)

ด้วย data.table 1.9 และ dplyr พร้อม data frame:

            test replications elapsed relative user.self sys.self
2 data.tableGood            1    0.02      1.0      0.02     0.00
3          dplyr            1    0.04      2.0      0.04     0.00
4          sqldf            1    0.46     23.0      0.46     0.00
1      aggregate            1    6.11    305.5      6.10     0.02

ด้วย data.table 1.9 และ dplyr พร้อมตารางข้อมูล:

            test replications elapsed relative user.self sys.self
2 data.tableGood            1    0.02        1      0.02     0.00
3          dplyr            1    0.02        1      0.02     0.00
4          sqldf            1    0.44       22      0.43     0.02
1      aggregate            1    6.14      307      6.10     0.01

packageVersion("data.table")
[1] '1.9.0'
packageVersion("dplyr")
[1] '0.1.2'

เพื่อความสอดคล้องนี่คือต้นฉบับที่มีทั้งหมดและ data.table 1.9 และ dplyr โดยใช้ตารางข้อมูล:

            test replications elapsed relative user.self sys.self
5          dplyr            1    0.01        1      0.02     0.00
3 data.tableGood            1    0.02        2      0.01     0.00
6          sqldf            1    0.47       47      0.46     0.00
1      aggregate            1    6.16      616      6.16     0.00
2  data.tableBad            1   15.45     1545     15.38     0.01
4           plyr            1  110.23    11023     90.46    19.52

ฉันคิดว่าข้อมูลนี้เล็กเกินไปสำหรับ data.table และ dplyr ใหม่ :)

ชุดข้อมูลที่ใหญ่ขึ้น:

N <- 1e8
g1 <- sample(1:50000, N, replace = TRUE)
g2<- sample(1:50000, N, replace = TRUE)
d <- data.frame(x=sample(N,N), y=rnorm(N), g1, g2)

ใช้ ram ประมาณ 10-13GB เพื่อเก็บข้อมูลก่อนที่จะรันเกณฑ์มาตรฐาน

ผล:

            test replications elapsed relative user.self sys.self
1          dplyr            1   14.88        1      6.24     7.52
2 data.tableGood            1   28.41        1     18.55      9.4

พยายาม 1 พันล้าน แต่ระเบิดขึ้น 32GB จะจัดการได้อย่างไม่มีปัญหา


[แก้ไขโดย Arun] (dotcomken คุณช่วยเรียกใช้โค้ดนี้และวางผลการเปรียบเทียบได้ไหมขอบคุณ)

require(data.table)
require(dplyr)
require(rbenchmark)

N <- 1e8
g1 <- sample(1:50000, N, replace = TRUE)
g2 <- sample(1:50000, N, replace = TRUE)
d <- data.frame(x=sample(N,N), y=rnorm(N), g1, g2)

benchmark(replications = 5, order = "elapsed", 
  data.table = {
     dt <- as.data.table(d) 
     dt[, lapply(.SD, mean), by = "g1,g2"]
  }, 
  dplyr_DF = d %.% group_by(g1, g2) %.% summarise(avx = mean(x), avy=mean(y))
) 

ตามคำขอของอรุณที่นี่ผลลัพธ์ของสิ่งที่คุณให้ฉันเรียกใช้:

        test replications elapsed relative user.self sys.self
1 data.table            5   15.35     1.00     13.77     1.57
2   dplyr_DF            5  137.84     8.98    136.31     1.44

ขออภัยสำหรับความสับสนตอนดึกมาหาฉัน

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


1
การปรับปรุงที่ดี ขอบคุณ. ฉันคิดว่าเครื่องของคุณเป็นสัตว์ร้ายเมื่อเทียบกับชุดข้อมูลนี้ .. แคช L2 ของคุณมีขนาดเท่าไหร่ (และ L3 ถ้ามี)?
อรุณ

i7 L2 คือ 2x256 KB 8 ทาง, L3 คือ 4 MB 16 ทาง SSD 128 GB, Win 7 บน Dell inspiron
dotcomken

1
คุณสามารถฟอร์แมตตัวอย่างใหม่ได้ไหม ฉันสับสนนิดหน่อย data.table ดีกว่า (ในตัวอย่างนี้) กว่า dplyr หรือไม่ ถ้าเป็นเช่นนั้นภายใต้สถานการณ์ใด
csgillespie

1

โดยใช้ฟังก์ชันผสานและพารามิเตอร์เสริม:

การรวมภายใน: การผสาน (df1, df2) จะใช้ได้กับตัวอย่างเหล่านี้เนื่องจาก R จะรวมเฟรมโดยอัตโนมัติด้วยชื่อตัวแปรทั่วไป แต่คุณมักต้องการระบุการผสาน (df1, df2, by = "CustomerId") เพื่อให้แน่ใจว่าคุณ มีการจับคู่เฉพาะฟิลด์ที่คุณต้องการ คุณยังสามารถใช้พารามิเตอร์ by.x และ by.y ได้หากตัวแปรที่ตรงกันมีชื่อต่างกันในกรอบข้อมูลที่ต่างกัน

Outer join: merge(x = df1, y = df2, by = "CustomerId", all = TRUE)

Left outer: merge(x = df1, y = df2, by = "CustomerId", all.x = TRUE)

Right outer: merge(x = df1, y = df2, by = "CustomerId", all.y = TRUE)

Cross join: merge(x = df1, y = df2, by = NULL)

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