小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析

1. 开场白

大家好,小护士在这里祝大家国庆节快乐。国庆七日长假,小护士没有出去浪,留守在80块键盘前搞数据分析。本篇是小护士第一篇写关于数据分析相关的技术干货,分享数据采集思路、数据清洗技巧以及数据分析套路,还有踩过的填过的坑。在这里,先感谢房天下提供公开数据,也感谢各位读者提供技术指导,感谢CSDN提供平台支持。开场白不多说,现在进入正题。

2. 选题

本次选题是做房价的简单分析,非常入门。以房天下的新楼盘列表页数据作为数据来源,通过简单数据清洗处理,展示几张比较经典的图表作为数据报告。

3. 数据报告

按照CSDN博客的数据分析技术文章惯例,先把本次数据报告展示出来。

数据采样情况:

采集过程中,小护士也觉得惊讶,北京的数据太少了。

数据采样分布:
小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析-LMLPHP

城市房价均价对比:
小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析-LMLPHP

第一名居然是深圳,小护士觉得这个肯定是因为北京的数据样本比较少,或者深圳的房价样本方差比较大(数据值之间差异很大)。北京上海作为地域龙头,各自不相上下;广州真的不如北上深,杭州都快赶上来了;而成都则真的太良心价了。

城市房价综合对比:
小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析-LMLPHP

这种box图比较常见。北京虽然与上海不相上下,但是可以看出,北京因为中位数较低,Q1Q3间距较大,数据样本分布不集中,反映北京市内房价地区差异很大;而上海则是相反,中位数较高,Q1Q3间距较小,说明上海市内房价地区差异较小,各区发展平衡。而对于深圳,总体上来说,反映房价都很高。至于,广州、杭州和成都,小护士认为,他们的差距大概是一年到两年,也就是说,现在的广州就是两年后的杭州,现在的杭州就是两年后的成都,这个两年只是打个比方,用于说明差距而已。

北京各区均价对比:
小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析-LMLPHP

图表中有个小小意外,就是有个区的名字叫“城区街道办事处”。小护士也是一脸懵逼,估计是做数据清洗时,没有考虑一些个别情况。所以说,ETL是所有数据分析的奠基石,虽然枯燥,也要非常严谨才行。

单单看这个图,实际上只能大概反映各区情况,可以看出北京市内的地区差异真的很大。

上海各区均价对比:
小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析-LMLPHP

上海的各区房价分布相较于北京来说均衡得多。不过,虽说是有均衡的特点,但也同时说明上海整体房价都很高,买哪里都很贵。小护士就算有50K月薪也不敢买上海的房子啊。

广州各区均价对比:
小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析-LMLPHP

广州的房价情况,小护士最有发言权。小护士是地头蛇,对广州的情况比较了解。天河真的很贵,贵得当之无愧,广州塔、珠江新城、体育西路、石牌桥这些凡是来过广州都可以耳熟能详的地铁站,无一例外都是属于天河区。越秀作为第二贵,也是毫无疑问的,广州的市政机关以及上世纪90年代CBD都是在越秀区,房龄普遍跟荔湾区差不多老,也是广州市老三区之一,吃喝玩乐全在这里。海珠区也是老三区之一,有很多老掉牙的西关大屋,最近因为大力发展琶洲这些曾经的不毛之地,房价也是默默地超越了荔湾区。最后说说荔湾区,这里是小护士成长的地方,作为老三区里面的最后一名,小学中学名校林立,学区房是支撑荔湾区房价的主要原因,平均房价也刚刚踏进50000元每平米大关。

深圳各区均价对比:
小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析-LMLPHP

深圳房价在本次数据采样结果中是最高的。南山区不用说,它是程序员面试与被面试,相爱相杀的地方,腾讯总部也位于此,小护士读大学期间,很多深圳同学都来自华侨城中学,这里可以脑补一下深圳校服+校花校草+青春校园剧那种看一秒就能中毒的场景;这就是小护士对南山区的印象,虽然没去过,但作为深圳房价第一高可见一斑。

杭州各区均价对比:
小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析-LMLPHP

小护士对杭州的印象就是滨江和西湖,为什么下城的房价这么高。阿里巴巴总部就在西湖那边。杭州就是阿里,阿里就是杭州,小护士一直这么粗浅地定义杭州这十几年的发展,只要阿里发展了,杭州就发展了。

成都各区均价对比:
小护士青铜上分系列之《数据挖掘》第二篇Python房价入门分析-LMLPHP

小护士不太了解成都,锦江位居房价第一高,高新区仅次于第二。

总的来说,北上广深杭成,深圳房价最高、发展最强,北京上海紧跟其后,广州一直与北上深保持距离,而得益于互联网发展迅猛的杭州也直逼广州,成都作为最后一名也在努力涨涨涨。

所有的数据报告都在这里了,下面是开始讲解代码实现,分享技术干货和踩过的坑。

4. 数据采集

本次数据采集使用Selenium+ChromeDriver的方式完成。其中,ChromeDriver是小护士第一次用的Driver,以前一直用HTMLUnitDriver。配置ChromeDriver的时候非常需要注意Driver的版本与本地开发环境的Chrome浏览器版本,因为它们有对应关系;配置开发环境时,不要把自己原来的Chrome浏览器卸载了,因为新版的可能会一直崩溃或者闪退,小护士在这里就踩过坑。

下面是driver使用的部分代码片段:

def collect(driver: webdriver):
    li_elements = driver.find_elements_by_css_selector("#newhouse_loupai_list > ul > li")

    list = []

    for li in li_elements:
        houses = li.find_elements_by_css_selector("div > div.nlc_details")
        if len(houses) == 0:
            continue

        house = houses[0]
        house_name = house.find_element_by_css_selector("div.house_value > div > a").text
        house_type = house.find_element_by_css_selector("div.house_type").text
        house_address = house.find_element_by_css_selector("div.relative_message > div.address > a").text
        house_feature = str(house.find_element_by_css_selector("div.fangyuan").text).replace("\n", " ")
        house_prices = house.find_elements_by_css_selector("div.nhouse_price")
        house_price = ''
        if len(house_prices) != 0:
            house_price = str(house_prices[0].text).replace("\n", " ")

        print(
            house_name + ","
            + house_type + ","
            + house_address + ","
            + house_feature + ","
            + house_price)

        list.append({
            "house_name": house_name,
            "house_type": house_type,
            "house_address": house_address,
            "house_feature": house_feature,
            "house_price": house_price
        })

    return list

对于采集阶段来说,最重要是熟悉CSS Selector(CSS选择器),小护士主要是用这种方式来定位HTML标签位置;一开始由于不熟悉,一直在数据列表页和CSS的MDN文档之间来回切换,浪费了不少时间。而且,Selenium有个大坑,就是如果只用find_element_XXX 而不是 find_elements_XXX的话,会因为定位不到那个单一的特定标签而抛出异常;一开始,小护士还疯狂地 try-except-finally(try-catch);后来发现其实可以用find_elements_XXX先查出一个列表结果,看看列表的length是不是零来判定是否定位到标签,这种写法不用try-except-finally,代码优雅些许。

def city_collect(url, city, to_path):
    driver = webdriver.Chrome(executable_path='/home/lzf/repos/pylab/driver/chromedriver')

    driver.get(url);

    list = []
    while True:
        temp_list = collect(driver)
        list.extend(temp_list)

        second = random.randint(1, 2)
        time.sleep(second)

        next_a_s = driver.find_elements_by_css_selector(
            '#bx1 > div > div.contentListf.fl.clearfix > ul > li.floatr.rankWrap > div.otherpage > a:last-child')
        if len(next_a_s) == 0:
            break

        can_out = False
        count = 3
        while True:
            try:
                next_a_s[0].click()
                can_out = True
            except:
                can_out = False
                print("click error count[" + str(4 - count) + "] on url: " + driver.current_url)
            finally:
                count -= 1
                can_out = can_out or count == 0
            if can_out:
                break

    time.sleep(2)
    driver.quit()

    write(list, city, to_path)

对于列表页的跳转,小护士则是用定位下一页标签并触发点击事件的方式来完成。有时候,会因为网络原因,页面部分资源没有完全加载完,Selenium会直接跑出异常说Timeout;所以需要做while循环至少保证有三次尝试机会。做采集的时候,一定要注意安全,尽量模仿用户的浏览习惯,该点击的点击,跳转的时候不要跳太猛,该随机1-2秒停顿的就要停顿。用完driver就要立刻quit()掉,不要占用太多内存资源。

5. 数据清洗

数据清洗其实没有太多技术含量,最主要是写正则表达式匹配group,例如:

def get_address(source: str):
    p = re.compile(r'.*\[(.+)\]\s?([\u4e00-\u9fff\d]+\([\u4e00-\u9fff\d]+\)|[\u4e00-\u9fff\d]+)')
    m = p.match(source)
    if m is not None:
        g2 = m.group(2)
        if g2 is not None:
            return g2
        else:
            return ""
    else:
        return ""

def get_price(source: str):
    p = re.compile(r'^(\d+)元/㎡$')
    m = p.match(source)
    if m is not None:
        g1 = m.group(1)
        if g1 is not None:
            return int(g1)
        else:
            return 0
    else:
        return 0

其他倒是没什么了,清洗阶段,程序跑的飞快。只要不写那种几层循环的处理,一两秒就跑完了。

6. 数据展示

数据展示这块,小护士躺了很多坑。可能是因为刚接触pandas的原因,在使用pandas做图表展示的时候就遇到tkinter缺失,这个需要本地安装,不能直接pip install。安装完以后还需要重新启动Pycharm IDE,不然还会报错说找不到Module。此时,小护士以为一切都会好起来,结果遇到pandas读取csv时,数据不能做类型转换,读进来都是object类型,怎么配置都不行,都会以“不能安全地做类型转换”为由来报错。后来,小护士妥协了一下,用一些骚操作转换bool和numeric的值。

def read_csv():
    df = pd.read_csv(CSV_FILE, header=0)
    df.room1 = df.room1 == 'True'
    df.room2 = df.room2 == 'True'
    df.room3 = df.room3 == 'True'
    df.room4 = df.room4 == 'True'
    df.room5 = df.room5 == 'True'
    df.room5_above = df.room5_above == 'True'
    df.min_square = pd.to_numeric(df.min_square, errors='coerce')
    df.max_square = pd.to_numeric(df.max_square, errors='coerce')
    df.price = pd.to_numeric(df.price, errors='coerce')
    return df

下次小护士要试试从DB里面读取会不会直接支持正确的类型,反正直接从csv里面读取就遇到这个类型转换坑;小护士不推荐大家直接从csv里面读数据进来做分析。

本以为,csv解决了,下面就可以直接操作plot做图表了,结果遇到了matplotlib不支持中文问题,小护士尝试去解决过,后来发现那些label如果要支持中文,则也要逐个去set一次支持中文。瞬间万念俱灰。后来,在Stackoverflow里面看到有个老外说找找其他库试试吧,于是小护士就搜索了“top 10 python chart lib”。然后就找到了一个不错的轻量级图表库,名字叫:pygal

用pygal制作图表,还支持导出svg,这个对工程来说相当友好。不像matplotlib,只给你单机显示结果,还要手动保存图表为图片,每每想起,内心都会有十万个mmp。虽然pygal支持的图表类型没有前端的echart、highchart那些那么多,当然最牛逼最鼻祖的当属D3.js;而对于现在小护士用python粗浅搞搞数据分析来说,pygal已经足够使用了。

下面是pygal的部分代码使用:

def city_price_bar(df: pd.DataFrame):
    city_group = df.query('price > 0').groupby('city').price.mean()[:]
    bar = pygal.Bar(print_values=True, print_values_position='top', value_formatter=lambda x: '{:.2f}元'.format(x),
                    style=DefaultStyle)
    bar.title = '各城市新楼盘平均价格'
    i = 0
    while i < len(city_group):
        bar.add(city_group.index[i], city_group[i])
        i += 1
    bar.render_to_file(CHART_PATH + "city_price_bar.svg")

def district_price_bar(df: pd.DataFrame, code, name):
    frame = df.query('price > 0 and city == "' + name + '"')
    dg = frame.groupby('district').price.mean()[1:]
    i = 0
    list = []
    labels = []
    while i < len(dg):
        list.append(dg[i])
        labels.append(dg.index[i])
        i += 1

    tm = pygal.Bar(value_formatter=lambda x: '{:.0f}元'.format(x), style=CleanStyle)
    tm.title = name + '各区新楼盘平均价格对比'
    tm.x_labels = labels
    tm.x_label_rotation = 50
    tm.add('楼价', list)

    tm.render_to_file(CHART_PATH + code + "_price_bar.svg")

6. 总结

又到总结的时候了,凡事都要总结一下才有下一次的进步。小护士是从国庆放假才开始正式写python代码,之前都只是断断续续看一些官方Tutorial、Library Reference,到了动手的那一刻才发现自己什么都不会,很多代码都是直接谷歌来的,甚至会搜索:python3 &&(因为python不支持&&操作符,要用and)。

想到要写什么就尝试去写,for循环、if-else、while循环、try-catch忘了就去谷歌查,优先点击官方文档的结果链接。到了后面,就变成查第三方库的使用方式;例如,导出导入csv文件怎么写,selenium怎么用driver定位html标签,pandas怎么创建一个DataFrame,pygal怎么渲染一个图表。

每每想到要放弃却又因为在谷歌结果中找到哪怕一丝希望,又会不断地去尝试,接受永无止境的报错信息,直到把程序调通为止。小护士的学习方法就是如此,一个学习的正循环反馈真的很重要,就像轮子,反馈越快学得越快。

本次数据分析的代码先不开源到github了,因为是第一次写这种类型的程序。从采集、清洗再到数据分析一条龙,每个环节都不简单,如果数据量大了还要考虑数据存储成本问题,不只是简单的csv读写。

数据分析这个行当感觉挺好的,起码对产品经理有一定专业要求,小护士相信开发数据分析的需求相对会比开发业务的需求要走心很多。

7. 参考文献

8. 技术交流

QQ群:大宽宽的技术交流群(317060090)
QQ群:JAVA高级交流(329019348)

小护士目前在寻觅技术氛围良好的QQ交流群,如果您有推荐,记得在评论区留言。

今天一不小心写了9000多字,望君见谅。

10-06 17:25